"""色准(ΔE2000 / 标准色)相关纯算法。 Step 1 重构:从 pqAutomationApp.PQAutomationApp 中原样搬迁以下方法, 去掉 self 参数,改为模块级纯函数: - calculate_delta_e_2000 - calculate_color_accuracy - get_accuracy_color_standards """ import math import numpy as np D65_X = 0.3127 D65_Y = 0.3290 # 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): """ 返回图表/表格用的参考亮度(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: test_type: 测试类型 ("sdr_movie" 或 "hdr_movie") Returns: dict: {color_name: (x, y), ...} """ del test_type return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS} def _xyY_to_lab(x, y, Y): if y == 0: return 0.0, 0.0, 0.0 X = x * Y / y Z = (1 - x - y) * Y / y 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, 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) 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) C_bar = (C1 + C2) / 2.0 G = 0.5 * (1 - math.sqrt(C_bar ** 7 / (C_bar ** 7 + 25 ** 7))) a1_prime = a1 * (1 + G) a2_prime = a2 * (1 + G) C1_prime = math.sqrt(a1_prime ** 2 + b1 ** 2) C2_prime = math.sqrt(a2_prime ** 2 + b2 ** 2) C_bar_prime = (C1_prime + C2_prime) / 2.0 def calc_hue(a_prime, b): if a_prime == 0 and b == 0: return 0 h = math.atan2(b, a_prime) * 180 / math.pi if h < 0: h += 360 return h h1_prime = calc_hue(a1_prime, b1) h2_prime = calc_hue(a2_prime, b2) if C1_prime == 0 or C2_prime == 0: delta_h_prime = 0 else: delta_h = h2_prime - h1_prime if abs(delta_h) <= 180: delta_h_prime = delta_h elif delta_h > 180: delta_h_prime = delta_h - 360 else: delta_h_prime = delta_h + 360 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: H_bar_prime = (h1_prime + h2_prime - 360) / 2.0 delta_L_prime = L2 - L1 delta_C_prime = C2_prime - C1_prime delta_H_prime = ( 2 * math.sqrt(C1_prime * C2_prime) * math.sin(math.radians(delta_h_prime / 2.0)) ) S_L = 1 + (0.015 * (L_bar - 50) ** 2) / math.sqrt(20 + (L_bar - 50) ** 2) S_C = 1 + 0.045 * C_bar_prime T = ( 1 - 0.17 * math.cos(math.radians(H_bar_prime - 30)) + 0.24 * math.cos(math.radians(2 * H_bar_prime)) + 0.32 * math.cos(math.radians(3 * H_bar_prime + 6)) - 0.20 * math.cos(math.radians(4 * H_bar_prime - 63)) ) S_H = 1 + 0.015 * C_bar_prime * T delta_theta = 30 * math.exp(-(((H_bar_prime - 275) / 25) ** 2)) 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 = kC = kH = 1.0 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)) ) 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): """计算色差(简化版,欧氏距离 × 1000)""" delta_E = {} for color in measured.keys(): dx = measured[color][0] - standard[color][0] dy = measured[color][1] - standard[color][1] delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000 return delta_E