diff --git a/app/runner/test_runner.py b/app/runner/test_runner.py index 30906d1..408a0c2 100644 --- a/app/runner/test_runner.py +++ b/app/runner/test_runner.py @@ -989,8 +989,7 @@ def test_color_accuracy(self: "PQAutomationApp", test_type): self.log_gui.log(f"计算色准(ΔE 2000,Gamma {target_gamma})...", level="info") self.log_gui.log("=" * 50, level="separator") - # 获取标准 xy 坐标 - standards = self.get_accuracy_color_standards(test_type) + white_lv = measured_data_list[0][2] delta_e_values = [] color_patches = [] @@ -1000,14 +999,12 @@ def test_color_accuracy(self: "PQAutomationApp", test_type): measured_y = measured_data[1] measured_lv = measured_data[2] - standard_x, standard_y = standards.get(name, (0.3127, 0.3290)) - - delta_e = self.calculate_delta_e_2000( + delta_e = self.calculate_accuracy_delta_e_2000( + name, measured_x, measured_y, measured_lv, - standard_x, - standard_y, + white_lv, ) delta_e_values.append(delta_e) diff --git a/app/tests/color_accuracy.py b/app/tests/color_accuracy.py index 76646b8..68e477d 100644 --- a/app/tests/color_accuracy.py +++ b/app/tests/color_accuracy.py @@ -11,68 +11,126 @@ import math import numpy as np +D65_X = 0.3127 +D65_Y = 0.3290 -def calculate_delta_e_2000( - measured_x, measured_y, measured_lv, standard_x, standard_y -): +# Calman ColorChecker 参考 xy(与 Calman dE2000 对齐;比较时使用实测 Y 作为目标 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: - measured_x, measured_y: 测量的 xy 坐标 - measured_lv: 测量的亮度(cd/m²) - standard_x, standard_y: 标准的 xy 坐标 + test_type: 测试类型 ("sdr_movie" 或 "hdr_movie") 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): - if y == 0: - return 0, 0, 0 - X = x * Y / y - Z = (1 - x - y) * Y / y - return X, Y, Z - # 修复:使用实际测量的亮度 - X1, Y1, Z1 = xy_to_XYZ(measured_x, measured_y, measured_lv) +def _xyY_to_lab(x, y, Y): + if y == 0: + return 0.0, 0.0, 0.0 - # 修复:标准值使用相同的参考亮度(只比较色度差异) - X2, Y2, Z2 = xy_to_XYZ(standard_x, standard_y, measured_lv) + X = x * Y / y + Z = (1 - x - y) * Y / y + Xn, Yn, Zn = 95.047, 100.000, 108.883 - # ========== 2. XYZ → Lab(D65 白点)========== - def XYZ_to_Lab(X, Y, Z): - # D65 白点 - Xn, Yn, Zn = 95.047, 100.000, 108.883 + def f(t): + delta = 6.0 / 29.0 + if t > delta ** 3: + return t ** (1.0 / 3.0) + return t / (3 * delta ** 2) + 4.0 / 29.0 - # 归一化 - xr = X / Xn - yr = Y / Yn - zr = Z / Zn + xr, yr, zr = X / Xn, Y / Yn, Z / Zn + fx, fy, fz = f(xr), f(yr), f(zr) + return 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz) - # f(t) 函数 - def f(t): - delta = 6.0 / 29.0 - if t > delta ** 3: - return t ** (1.0 / 3.0) - else: - return t / (3 * delta ** 2) + 4.0 / 29.0 - fx = f(xr) - fy = f(yr) - fz = f(zr) - - L = 116 * fy - 16 - a = 500 * (fx - fy) - b = 200 * (fy - fz) - - return L, a, b - - L1, a1, b1 = XYZ_to_Lab(X1, Y1, Z1) - L2, a2, b2 = XYZ_to_Lab(X2, Y2, Z2) - - # ========== 3. ΔE 2000 公式 ========== +def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2): L_bar = (L1 + L2) / 2.0 C1 = math.sqrt(a1 ** 2 + b1 ** 2) C2 = math.sqrt(a2 ** 2 + b2 ** 2) @@ -111,13 +169,12 @@ def calculate_delta_e_2000( if C1_prime == 0 or C2_prime == 0: H_bar_prime = h1_prime + h2_prime + elif abs(h1_prime - h2_prime) <= 180: + H_bar_prime = (h1_prime + h2_prime) / 2.0 + elif h1_prime + h2_prime < 360: + H_bar_prime = (h1_prime + h2_prime + 360) / 2.0 else: - if abs(h1_prime - h2_prime) <= 180: - H_bar_prime = (h1_prime + h2_prime) / 2.0 - elif h1_prime + h2_prime < 360: - H_bar_prime = (h1_prime + h2_prime + 360) / 2.0 - else: - H_bar_prime = (h1_prime + h2_prime - 360) / 2.0 + H_bar_prime = (h1_prime + h2_prime - 360) / 2.0 delta_L_prime = L2 - L1 delta_C_prime = C2_prime - C1_prime @@ -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_T = -R_C * math.sin(math.radians(2 * delta_theta)) - kL = 1.0 - kC = 1.0 - kH = 1.0 + kL = kC = kH = 1.0 - delta_E = math.sqrt( + return math.sqrt( (delta_L_prime / (kL * S_L)) ** 2 + (delta_C_prime / (kC * S_C)) ** 2 + (delta_H_prime / (kH * S_H)) ** 2 + 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 +): + """ + 色准测试专用 ΔE2000(Calman 对齐)。 + + 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): @@ -168,81 +270,3 @@ def calculate_color_accuracy(measured, standard): delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000 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 → XYZ(D65 白点,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 diff --git a/app/views/pq_debug_panel.py b/app/views/pq_debug_panel.py index f46a911..5c182a3 100644 --- a/app/views/pq_debug_panel.py +++ b/app/views/pq_debug_panel.py @@ -1175,18 +1175,17 @@ class PQDebugPanel: ): """计算单个色块的 ΔE 2000""" try: - # 获取标准 xy 坐标 - test_type = self.current_test_type - standards = self.app.get_accuracy_color_standards(test_type) + white_lv = measured_lv + measured = self.app.results.get_intermediate_data("accuracy", "measured") + if measured and len(measured) > 0 and measured[0][2]: + white_lv = measured[0][2] - if color_name not in standards: - return 0.0 - - standard_x, standard_y = standards[color_name] - - # 调用主程序的 ΔE 计算方法 - delta_e = self.app.calculate_delta_e_2000( - measured_x, measured_y, measured_lv, standard_x, standard_y + delta_e = self.app.calculate_accuracy_delta_e_2000( + color_name, + measured_x, + measured_y, + measured_lv, + white_lv, ) return delta_e diff --git a/pqAutomationApp.py b/pqAutomationApp.py index c7d3a1f..c1cac03 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -44,6 +44,7 @@ from app.resources import ( load_icon, ) 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_delta_e_2000 as _calc_delta_e_2000, get_accuracy_color_standards as _get_accuracy_color_standards, @@ -740,6 +741,7 @@ class PQAutomationApp( # 纯算法函数:作为 staticmethod 保留在主类(不依赖 self,且 calculate_xxx # 的命名空间由历史代码以 self.calculate_xxx 调用)。 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) calculate_gamut_coverage = staticmethod(_calc_gamut_coverage) calculate_gamma = staticmethod(_calc_gamma)