Compare commits

...

2 Commits

Author SHA1 Message Date
xinzhu.yin
dc096a9805 修复UCD调用异常、保存结果按钮异常 2026-06-15 10:58:51 +08:00
xinzhu.yin
c8ad244c45 修改SDR色准dE计算方式 2026-06-12 16:46:30 +08:00
8 changed files with 242 additions and 170 deletions

View File

@@ -989,8 +989,7 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
self.log_gui.log(f"计算色准ΔE 2000Gamma {target_gamma}...", level="info") self.log_gui.log(f"计算色准ΔE 2000Gamma {target_gamma}...", level="info")
self.log_gui.log("=" * 50, level="separator") self.log_gui.log("=" * 50, level="separator")
# 获取标准 xy 坐标 white_lv = measured_data_list[0][2]
standards = self.get_accuracy_color_standards(test_type)
delta_e_values = [] delta_e_values = []
color_patches = [] color_patches = []
@@ -1000,14 +999,12 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
measured_y = measured_data[1] measured_y = measured_data[1]
measured_lv = measured_data[2] measured_lv = measured_data[2]
standard_x, standard_y = standards.get(name, (0.3127, 0.3290)) delta_e = self.calculate_accuracy_delta_e_2000(
name,
delta_e = self.calculate_delta_e_2000(
measured_x, measured_x,
measured_y, measured_y,
measured_lv, measured_lv,
standard_x, white_lv,
standard_y,
) )
delta_e_values.append(delta_e) delta_e_values.append(delta_e)

View File

@@ -11,68 +11,126 @@ import math
import numpy as np import numpy as np
D65_X = 0.3127
D65_Y = 0.3290
def calculate_delta_e_2000( # Calman ColorChecker 参考 xy与 Calman dE2000 对齐;比较时使用实测 Y 作为目标 Y
measured_x, measured_y, measured_lv, standard_x, standard_y _ACCURACY_REFERENCE_XY = {
): "White": (0.3127, 0.3282),
"Gray 80": (0.3128, 0.3283),
"Gray 65": (0.3118, 0.3270),
"Gray 50": (0.3122, 0.3282),
"Gray 35": (0.3124, 0.3278),
"Dark Skin": (0.4042, 0.3686),
"Light Skin": (0.3774, 0.3562),
"Blue Sky": (0.2535, 0.2671),
"Foliage": (0.3379, 0.4287),
"Blue Flower": (0.2691, 0.2484),
"Bluish Green": (0.2578, 0.3544),
"Orange": (0.5047, 0.4088),
"Purplish Blue": (0.2166, 0.1857),
"Moderate Red": (0.4554, 0.3098),
"Purple": (0.2889, 0.2135),
"Yellow Green": (0.3771, 0.4937),
"Orange Yellow": (0.4578, 0.4416),
"Blue (Legacy)": (0.1851, 0.1238),
"Green (Legacy)": (0.3008, 0.4976),
"Red (Legacy)": (0.5435, 0.3200),
"Yellow (Legacy)": (0.4430, 0.4717),
"Magenta (Legacy)": (0.3735, 0.2428),
"Cyan (Legacy)": (0.2093, 0.2679),
"100% Red": (0.6424, 0.3274),
"100% Green": (0.2935, 0.6024),
"100% Blue": (0.1615, 0.0610),
"100% Cyan": (0.2302, 0.3340),
"100% Magenta": (0.3300, 0.1513),
"100% Yellow": (0.4152, 0.5047),
}
# 29 色 SDR 标准色板Legacy 色块仍保留 RGB 定义供图案发送)
_SDR_COLOR_PATTERNS = [
("White", 255, 255, 255),
("Gray 80", 230, 230, 230),
("Gray 65", 209, 209, 209),
("Gray 50", 186, 186, 186),
("Gray 35", 158, 158, 158),
("Dark Skin", 115, 82, 66),
("Light Skin", 194, 150, 130),
("Blue Sky", 94, 122, 156),
("Foliage", 89, 107, 66),
("Blue Flower", 130, 128, 176),
("Bluish Green", 99, 189, 168),
("Orange", 217, 120, 41),
("Purplish Blue", 74, 92, 163),
("Moderate Red", 194, 84, 97),
("Purple", 92, 61, 107),
("Yellow Green", 158, 186, 64),
("Orange Yellow", 230, 161, 46),
("Blue (Legacy)", 51, 61, 150),
("Green (Legacy)", 71, 148, 71),
("Red (Legacy)", 176, 48, 59),
("Yellow (Legacy)", 237, 199, 33),
("Magenta (Legacy)", 186, 84, 145),
("Cyan (Legacy)", 0, 133, 163),
("100% Red", 255, 0, 0),
("100% Green", 0, 255, 0),
("100% Blue", 0, 0, 255),
("100% Cyan", 0, 255, 255),
("100% Magenta", 255, 0, 255),
("100% Yellow", 255, 255, 0),
]
def _resolve_reference_xy(name):
return _ACCURACY_REFERENCE_XY.get(name, (D65_X, D65_Y))
def get_accuracy_reference_y(name, white_lv):
""" """
计算 ΔE 2000 色差(修正版) 返回图表/表格用的参考亮度Calman 目标 Y 比例White=100 缩放)。
注意ΔE2000 计算使用实测 Y 作为目标 Y与 Calman 一致),此函数仅供展示。
"""
del name
if white_lv <= 0:
return 100.0
return white_lv
def get_accuracy_color_standards(test_type):
"""
获取色准测试的标准 xy 色度坐标Calman 兼容参考值)。
Args: Args:
measured_x, measured_y: 测量的 xy 坐标 test_type: 测试类型 ("sdr_movie""hdr_movie")
measured_lv: 测量的亮度cd/m²
standard_x, standard_y: 标准的 xy 坐标
Returns: Returns:
float: ΔE 2000 色差值 dict: {color_name: (x, y), ...}
""" """
del test_type
return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS}
# ========== 1. xy → XYZ使用实际亮度==========
def xy_to_XYZ(x, y, Y): def _xyY_to_lab(x, y, Y):
if y == 0: if y == 0:
return 0, 0, 0 return 0.0, 0.0, 0.0
X = x * Y / y X = x * Y / y
Z = (1 - x - y) * Y / y Z = (1 - x - y) * Y / y
return X, Y, Z
# 修复:使用实际测量的亮度
X1, Y1, Z1 = xy_to_XYZ(measured_x, measured_y, measured_lv)
# 修复:标准值使用相同的参考亮度(只比较色度差异)
X2, Y2, Z2 = xy_to_XYZ(standard_x, standard_y, measured_lv)
# ========== 2. XYZ → LabD65 白点)==========
def XYZ_to_Lab(X, Y, Z):
# D65 白点
Xn, Yn, Zn = 95.047, 100.000, 108.883 Xn, Yn, Zn = 95.047, 100.000, 108.883
# 归一化
xr = X / Xn
yr = Y / Yn
zr = Z / Zn
# f(t) 函数
def f(t): def f(t):
delta = 6.0 / 29.0 delta = 6.0 / 29.0
if t > delta ** 3: if t > delta ** 3:
return t ** (1.0 / 3.0) return t ** (1.0 / 3.0)
else:
return t / (3 * delta ** 2) + 4.0 / 29.0 return t / (3 * delta ** 2) + 4.0 / 29.0
fx = f(xr) xr, yr, zr = X / Xn, Y / Yn, Z / Zn
fy = f(yr) fx, fy, fz = f(xr), f(yr), f(zr)
fz = f(zr) return 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)
L = 116 * fy - 16
a = 500 * (fx - fy)
b = 200 * (fy - fz)
return L, a, b def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2):
L1, a1, b1 = XYZ_to_Lab(X1, Y1, Z1)
L2, a2, b2 = XYZ_to_Lab(X2, Y2, Z2)
# ========== 3. ΔE 2000 公式 ==========
L_bar = (L1 + L2) / 2.0 L_bar = (L1 + L2) / 2.0
C1 = math.sqrt(a1 ** 2 + b1 ** 2) C1 = math.sqrt(a1 ** 2 + b1 ** 2)
C2 = math.sqrt(a2 ** 2 + b2 ** 2) C2 = math.sqrt(a2 ** 2 + b2 ** 2)
@@ -111,8 +169,7 @@ def calculate_delta_e_2000(
if C1_prime == 0 or C2_prime == 0: if C1_prime == 0 or C2_prime == 0:
H_bar_prime = h1_prime + h2_prime H_bar_prime = h1_prime + h2_prime
else: elif abs(h1_prime - h2_prime) <= 180:
if abs(h1_prime - h2_prime) <= 180:
H_bar_prime = (h1_prime + h2_prime) / 2.0 H_bar_prime = (h1_prime + h2_prime) / 2.0
elif h1_prime + h2_prime < 360: elif h1_prime + h2_prime < 360:
H_bar_prime = (h1_prime + h2_prime + 360) / 2.0 H_bar_prime = (h1_prime + h2_prime + 360) / 2.0
@@ -144,18 +201,63 @@ def calculate_delta_e_2000(
R_C = 2 * math.sqrt(C_bar_prime ** 7 / (C_bar_prime ** 7 + 25 ** 7)) R_C = 2 * math.sqrt(C_bar_prime ** 7 / (C_bar_prime ** 7 + 25 ** 7))
R_T = -R_C * math.sin(math.radians(2 * delta_theta)) R_T = -R_C * math.sin(math.radians(2 * delta_theta))
kL = 1.0 kL = kC = kH = 1.0
kC = 1.0
kH = 1.0
delta_E = math.sqrt( return math.sqrt(
(delta_L_prime / (kL * S_L)) ** 2 (delta_L_prime / (kL * S_L)) ** 2
+ (delta_C_prime / (kC * S_C)) ** 2 + (delta_C_prime / (kC * S_C)) ** 2
+ (delta_H_prime / (kH * S_H)) ** 2 + (delta_H_prime / (kH * S_H)) ** 2
+ R_T * (delta_C_prime / (kC * S_C)) * (delta_H_prime / (kH * S_H)) + R_T * (delta_C_prime / (kC * S_C)) * (delta_H_prime / (kH * S_H))
) )
return delta_E
def calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
standard_lv=None,
):
"""
计算 ΔE 2000 色差。
Args:
measured_x, measured_y: 测量的 xy 坐标
measured_lv: 测量的亮度cd/m²
standard_x, standard_y: 标准的 xy 坐标
standard_lv: 标准亮度cd/m²默认与 measured_lv 相同
Returns:
float: ΔE 2000 色差值
"""
if standard_lv is None:
standard_lv = measured_lv
L1, a1, b1 = _xyY_to_lab(measured_x, measured_y, measured_lv)
L2, a2, b2 = _xyY_to_lab(standard_x, standard_y, standard_lv)
return _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2)
def calculate_accuracy_delta_e_2000(
patch_name, measured_x, measured_y, measured_lv, white_lv
):
"""
色准测试专用 ΔE2000Calman 对齐)。
Calman 在 ColorChecker 测试中对每块使用固定参考 xy
且目标 Y 取实测 Y同亮度下比较色度差异
"""
del white_lv
standard_x, standard_y = _resolve_reference_xy(patch_name)
return calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
measured_lv,
)
def calculate_color_accuracy(measured, standard): def calculate_color_accuracy(measured, standard):
@@ -168,81 +270,3 @@ def calculate_color_accuracy(measured, standard):
delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000 delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000
return delta_E return delta_E
# 29 色 SDR 标准色板(保持与原实现一致)
_SDR_COLOR_PATTERNS = [
("White", 255, 255, 255),
("Gray 80", 230, 230, 230),
("Gray 65", 209, 209, 209),
("Gray 50", 186, 186, 186),
("Gray 35", 158, 158, 158),
("Dark Skin", 115, 82, 66),
("Light Skin", 194, 150, 130),
("Blue Sky", 94, 122, 156),
("Foliage", 89, 107, 66),
("Blue Flower", 130, 128, 176),
("Bluish Green", 99, 189, 168),
("Orange", 217, 120, 41),
("Purplish Blue", 74, 92, 163),
("Moderate Red", 194, 84, 97),
("Purple", 92, 61, 107),
("Yellow Green", 158, 186, 64),
("Orange Yellow", 230, 161, 46),
("Blue (Legacy)", 51, 61, 150),
("Green (Legacy)", 71, 148, 71),
("Red (Legacy)", 176, 48, 59),
("Yellow (Legacy)", 237, 199, 33),
("Magenta (Legacy)", 186, 84, 145),
("Cyan (Legacy)", 0, 133, 163),
("100% Red", 255, 0, 0),
("100% Green", 0, 255, 0),
("100% Blue", 0, 0, 255),
("100% Cyan", 0, 255, 255),
("100% Magenta", 255, 0, 255),
("100% Yellow", 255, 255, 0),
]
def _rgb_to_xy_srgb(r, g, b):
"""sRGB (8bit) → CIE 1931 xy"""
r, g, b = r / 255.0, g / 255.0, b / 255.0
def gamma_decode(c):
if c <= 0.04045:
return c / 12.92
else:
return ((c + 0.055) / 1.055) ** 2.4
r_linear = gamma_decode(r)
g_linear = gamma_decode(g)
b_linear = gamma_decode(b)
# sRGB → XYZD65 白点IEC 61966-2-1
X = r_linear * 0.4124564 + g_linear * 0.3575761 + b_linear * 0.1804375
Y = r_linear * 0.2126729 + g_linear * 0.7151522 + b_linear * 0.0721750
Z = r_linear * 0.0193339 + g_linear * 0.1191920 + b_linear * 0.9503041
total = X + Y + Z
if total == 0:
return 0.3127, 0.3290 # D65 白点
return X / total, Y / total
def get_accuracy_color_standards(test_type):
"""
获取色准测试的标准 xy 色度坐标(动态计算)
Args:
test_type: 测试类型 ("sdr_movie""hdr_movie")
Returns:
dict: {color_name: (x, y), ...}
"""
# 注意:原实现对 sdr/hdr 使用同一张色板,这里保持原行为。
del test_type # 参数保留以兼容调用方签名
standards = {}
for name, r, g, b in _SDR_COLOR_PATTERNS:
standards[name] = _rgb_to_xy_srgb(r, g, b)
return standards

View File

@@ -4,11 +4,18 @@ from __future__ import annotations
import logging import logging
from app.ucd.domain import ( from app.ucd.domain import (
Colorimetry,
DynamicRange,
EventBus, EventBus,
PatternKind,
PatternSpec, PatternSpec,
SignalFormat, SignalFormat,
TimingSpec, TimingSpec,
UcdError,
UcdState, UcdState,
build_signal_format,
build_signal_format_from_profile,
build_timing,
image_pattern, image_pattern,
solid_rgb_pattern, solid_rgb_pattern,
) )

View File

@@ -97,6 +97,12 @@ def hide_all_panels(self: "PQAutomationApp"):
self.current_panel = None self.current_panel = None
if hasattr(self, "_sync_save_button_state"):
try:
self._sync_save_button_state()
except Exception:
pass

View File

@@ -1087,12 +1087,7 @@ def _show_list_context_menu(self: "PQAutomationApp", event):
_switch_to_session(self, sid, show_message=False, refresh_list=False) _switch_to_session(self, sid, show_message=False, refresh_list=False)
has_selection = self.ai_image_current is not None has_selection = self.ai_image_current is not None
ucd = getattr(self, "ucd", None) can_send = has_selection and self.is_ucd_connected
can_send = (
has_selection
and ucd is not None
and getattr(ucd, "status", False)
)
try: try:
self.ai_image_menu.entryconfigure( self.ai_image_menu.entryconfigure(
0, state=("normal" if can_send else "disabled") 0, state=("normal" if can_send else "disabled")
@@ -1120,8 +1115,7 @@ def _send_to_ucd(self: "PQAutomationApp"):
if not os.path.isfile(rec.image_path): if not os.path.isfile(rec.image_path):
messagebox.showerror("错误", f"图片文件不存在:\n{rec.image_path}") messagebox.showerror("错误", f"图片文件不存在:\n{rec.image_path}")
return return
ucd = getattr(self, "ucd", None) if not self.is_ucd_connected:
if ucd is None or not getattr(ucd, "status", False):
messagebox.showwarning("警告", "请先连接 UCD323 设备") messagebox.showwarning("警告", "请先连接 UCD323 设备")
return return

View File

@@ -722,7 +722,7 @@ class PQDebugPanel:
test_item: gamma / eotf / accuracy / rgb test_item: gamma / eotf / accuracy / rgb
""" """
# 检查设备连接 # 检查设备连接
if not self.app.ca or not self.app.ucd: if not self.app.ca or not self.app.is_ucd_connected:
messagebox.showerror("错误", "请先连接设备") messagebox.showerror("错误", "请先连接设备")
return return
@@ -1175,18 +1175,17 @@ class PQDebugPanel:
): ):
"""计算单个色块的 ΔE 2000""" """计算单个色块的 ΔE 2000"""
try: try:
# 获取标准 xy 坐标 white_lv = measured_lv
test_type = self.current_test_type measured = self.app.results.get_intermediate_data("accuracy", "measured")
standards = self.app.get_accuracy_color_standards(test_type) if measured and len(measured) > 0 and measured[0][2]:
white_lv = measured[0][2]
if color_name not in standards: delta_e = self.app.calculate_accuracy_delta_e_2000(
return 0.0 color_name,
measured_x,
standard_x, standard_y = standards[color_name] measured_y,
measured_lv,
# 调用主程序的 ΔE 计算方法 white_lv,
delta_e = self.app.calculate_delta_e_2000(
measured_x, measured_y, measured_lv, standard_x, standard_y
) )
return delta_e return delta_e

View File

@@ -44,6 +44,7 @@ from app.resources import (
load_icon, load_icon,
) )
from app.tests.color_accuracy import ( from app.tests.color_accuracy import (
calculate_accuracy_delta_e_2000 as _calc_accuracy_delta_e_2000,
calculate_color_accuracy as _calc_color_accuracy, calculate_color_accuracy as _calc_color_accuracy,
calculate_delta_e_2000 as _calc_delta_e_2000, calculate_delta_e_2000 as _calc_delta_e_2000,
get_accuracy_color_standards as _get_accuracy_color_standards, get_accuracy_color_standards as _get_accuracy_color_standards,
@@ -66,6 +67,16 @@ from app.runner.test_runner import TestRunnerMixin
plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
class _LegacyUcdConnected:
"""Shim for legacy ``self.ucd.status`` checks after the UCD service refactor."""
status = True
_LEGACY_UCD_CONNECTED = _LegacyUcdConnected()
class PQAutomationApp( class PQAutomationApp(
ConfigIOMixin, ConfigIOMixin,
ChartFrameMixin, ChartFrameMixin,
@@ -206,6 +217,7 @@ class PQAutomationApp(
# 创建测试类型选择区域 # 创建测试类型选择区域
self.create_test_type_frame() self.create_test_type_frame()
self._setup_connection_event_handlers() self._setup_connection_event_handlers()
self.refresh_connection_indicators()
# 创建操作按钮区域 # 创建操作按钮区域
self.create_operation_frame() self.create_operation_frame()
# 创建结果图表区域 # 创建结果图表区域
@@ -234,6 +246,18 @@ class PQAutomationApp(
anchor=tk.E, anchor=tk.E,
).pack(side=tk.RIGHT) ).pack(side=tk.RIGHT)
@property
def is_ucd_connected(self) -> bool:
"""UCD323 是否已连接(统一入口,替代旧的 ``self.ucd.status``)。"""
return self.signal_service.is_connected
@property
def ucd(self):
"""Legacy UCD handle: ``None`` when disconnected, truthy object with ``.status`` when connected."""
if not self.is_ucd_connected:
return None
return _LEGACY_UCD_CONNECTED
def _setup_connection_event_handlers(self) -> None: def _setup_connection_event_handlers(self) -> None:
"""订阅连接事件,驱动 UCD / CA 指示灯(替代轮询 controller.status""" """订阅连接事件,驱动 UCD / CA 指示灯(替代轮询 controller.status"""
@@ -462,19 +486,38 @@ class PQAutomationApp(
self._chart_snapshots[test_type] = {} self._chart_snapshots[test_type] = {}
self._chart_snapshots[test_type][chart_name] = args self._chart_snapshots[test_type][chart_name] = args
def _has_saveable_results(self, test_type: str) -> bool:
"""判断指定测试类型是否有可保存的历史结果。"""
snapshots = self._chart_snapshots.get(test_type) or {}
return bool(snapshots) or (
hasattr(self, "results") and self.results.has(test_type)
)
def _sync_save_button_state(self):
"""根据当前测试类型是否有可保存结果,同步保存按钮状态。"""
if not hasattr(self, "save_btn"):
return
if getattr(self, "testing", False):
self.save_btn.config(state=tk.DISABLED)
return
test_type = self.test_type_var.get() if hasattr(self, "test_type_var") else None
if not test_type:
return
state = tk.NORMAL if self._has_saveable_results(test_type) else tk.DISABLED
self.save_btn.config(state=state)
def _restore_charts_for_type(self, test_type: str): def _restore_charts_for_type(self, test_type: str):
""" """
切换测试类型后恢复图表显示: 切换测试类型后恢复图表显示:
- 该类型有历史结果 → 切换活跃结果 + 重绘所有已缓存图表 - 该类型有历史结果 → 切换活跃结果 + 重绘所有已缓存图表 + 启用保存按钮
- 该类型无历史结果 → 清空图表 + 禁用保存按钮 - 该类型无历史结果 → 清空图表 + 禁用保存按钮
""" """
self.results.set_active(test_type) self.results.set_active(test_type)
snapshots = self._chart_snapshots.get(test_type) if not self._has_saveable_results(test_type):
if not snapshots:
self.clear_chart() self.clear_chart()
if hasattr(self, "save_btn"): self._sync_save_button_state()
self.save_btn.config(state=tk.DISABLED)
return return
snapshots = self._chart_snapshots.get(test_type) or {}
for chart_name, args in snapshots.items(): for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None) plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn: if plot_fn:
@@ -482,6 +525,7 @@ class PQAutomationApp(
plot_fn(*args) plot_fn(*args)
except Exception: except Exception:
pass pass
self._sync_save_button_state()
def _check_start_preconditions(self): def _check_start_preconditions(self):
"""检查开始测试前置条件:设备连接 & 未在测试中。""" """检查开始测试前置条件:设备连接 & 未在测试中。"""
@@ -740,6 +784,7 @@ class PQAutomationApp(
# 纯算法函数:作为 staticmethod 保留在主类(不依赖 self且 calculate_xxx # 纯算法函数:作为 staticmethod 保留在主类(不依赖 self且 calculate_xxx
# 的命名空间由历史代码以 self.calculate_xxx 调用)。 # 的命名空间由历史代码以 self.calculate_xxx 调用)。
calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000) calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
calculate_accuracy_delta_e_2000 = staticmethod(_calc_accuracy_delta_e_2000)
get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards) get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
calculate_gamut_coverage = staticmethod(_calc_gamut_coverage) calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
calculate_gamma = staticmethod(_calc_gamma) calculate_gamma = staticmethod(_calc_gamma)

View File

@@ -1,5 +1,5 @@
{ {
"current_test_type": "screen_module", "current_test_type": "sdr_movie",
"test_types": { "test_types": {
"screen_module": { "screen_module": {
"name": "屏模组性能测试", "name": "屏模组性能测试",