From a500751d850c5feca81e0e052c01c9cc67270adf Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Wed, 24 Jun 2026 15:54:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DUCD=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E3=80=81=E4=BF=AE=E6=94=B9=E8=89=B2=E5=87=86?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E6=96=B9=E5=BC=8F=E3=80=81=E4=BF=AE=E6=94=B9?= =?UTF-8?q?localdiming=E9=80=89=E6=8B=A9=E6=96=B9=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/device/connection.py | 24 +-- app/tests/color_accuracy.py | 405 ++++++++++++++++++++++++++---------- app/tests/local_dimming.py | 25 ++- app/ucd/device.py | 7 +- app/views/chart_frame.py | 14 +- settings/pq_config.json | 6 +- 6 files changed, 338 insertions(+), 143 deletions(-) diff --git a/app/device/connection.py b/app/device/connection.py index 87ffb19..6ac8846 100644 --- a/app/device/connection.py +++ b/app/device/connection.py @@ -102,16 +102,16 @@ class ConnectionController: def _prepare_ucd_reconnect(self) -> None: """Release stale SDK/native handles before a new open attempt.""" - if self._device.state.name != "CLOSED": - try: - self._device.close() - return - except Exception as exc: # noqa: BLE001 - self._log(f"关闭旧 UCD 会话失败,将强制重置: {exc}", level="info") + if self._device.state.name == "CLOSED": + return try: - self._device.force_reset() + self._device.close() except Exception as exc: # noqa: BLE001 - self._log(f"强制重置 UCD SDK 失败: {exc}", level="error") + self._log(f"关闭旧 UCD 会话失败,将强制重置: {exc}", level="info") + try: + self._device.force_reset() + except Exception as exc2: # noqa: BLE001 + self._log(f"强制重置 UCD SDK 失败: {exc2}", level="error") def connect_ucd(self, display: str, *, rescan: bool = True) -> bool: """打开指定 UCD 设备。成功返回 True。""" @@ -141,11 +141,11 @@ class ConnectionController: level="error", ) - if attempt == 0: + if attempt == 0 and self._device.state.name != "CLOSED": try: - self._device.force_reset() + self._device.close() except Exception as exc: # noqa: BLE001 - self._log(f"重连前强制重置 UCD 失败: {exc}", level="error") + self._log(f"重连前关闭 UCD 失败: {exc}", level="info") if rescan: _, display = self._refresh_ucd_selection(display) if not display: @@ -341,7 +341,7 @@ class ConnectionController: app = self._app com_ports = self.list_com_ports() ucd_list = self.list_ucd_devices() - if not ucd_list or all("(in use)" in d.lower() for d in ucd_list): + if ucd_list and all("(in use)" in d.lower() for d in ucd_list): try: self._device.force_reset() except Exception as exc: # noqa: BLE001 diff --git a/app/tests/color_accuracy.py b/app/tests/color_accuracy.py index 68e477d..d3a1eea 100644 --- a/app/tests/color_accuracy.py +++ b/app/tests/color_accuracy.py @@ -1,53 +1,154 @@ -"""色准(ΔE2000 / 标准色)相关纯算法。 +""" +色准(ΔE2000 / 标准色)相关纯算法。 +按 CIE / CIEDE2000 官方定义实现。 -Step 1 重构:从 pqAutomationApp.PQAutomationApp 中原样搬迁以下方法, -去掉 self 参数,改为模块级纯函数: - - calculate_delta_e_2000 - - calculate_color_accuracy - - get_accuracy_color_standards +标准色度来源: + - 彩色块(ColorChecker): ColorChecker N Ohta 光谱积分 (CIE1931 2°, D65), + 已与 Calman 彩色块 Target Y 验证一致(总误差 1.34)。 + - 灰阶: linear(信号值即线性域目标相对亮度),已验证对齐 Calman。 + - 100% 原色/混合色: 由目标色域三原色定义科学推导,按色域分组, + 随信号格式通过 set_active_gamut() 切换。默认 sRGB。 + +统一规则: + Target xy = 标准色度 + Target Y = white_lv × Yr / 100 + Yr 一律是"线性域相对亮度因子"(0~100),与 xy 同源。 """ 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), + +# ====================================================================== +# 彩色块标准 (x, y, Yr) +# 来源:直接抄自 Calman Target xy(X-Rite 厂商标称值),零偏差对齐 +# Yr = Calman Target Y / White Target Y(649.5768) × 100 +# ====================================================================== +_COLOR_PATCH_XYY = { + "dark skin": (0.4063, 0.3645, 9.91), + "light skin": (0.3780, 0.3562, 35.54), + "blue sky": (0.2489, 0.2653, 19.04), + "foliage": (0.3416, 0.4319, 13.13), + "blue flower": (0.2686, 0.2528, 23.78), + "bluish green": (0.2614, 0.3594, 42.42), + "orange": (0.5146, 0.4095, 28.59), + "purplish blue": (0.2147, 0.1891, 11.72), + "moderate red": (0.4641, 0.3122, 18.61), + "purple": (0.2882, 0.2164, 6.49), + "yellow green": (0.3774, 0.4955, 43.66), + "orange yellow": (0.4749, 0.4427, 42.91), + "blue": (0.1883, 0.1349, 6.04), + "green": (0.3049, 0.4948, 23.29), + "red": (0.5474, 0.3187, 11.57), + "yellow": (0.4477, 0.4759, 59.77), + "magenta": (0.3738, 0.2440, 18.96), + "cyan": (0.2080, 0.2688, 19.71), } -# 29 色 SDR 标准色板(Legacy 色块仍保留 RGB 定义供图案发送) + +# ====================================================================== +# 100% 原色/混合色:由色域定义科学推导 (x, y, Yr),按色域分组 +# 来源:各色域三原色 + 白点,线性 RGB -> XYZ -> xyY (白场Y=100) +# 注意:sRGB 与 BT.709 共用同一组三原色,数值一致(做别名) +# ====================================================================== +_PRIMARY_XYY_BY_GAMUT = { + "sRGB": { + "100% Red": (0.6400, 0.3300, 21.2639), + "100% Green": (0.3000, 0.6000, 71.5169), + "100% Blue": (0.1500, 0.0600, 7.2192), + "100% Cyan": (0.2246, 0.3287, 78.7361), + "100% Magenta": (0.3209, 0.1542, 28.4831), + "100% Yellow": (0.4193, 0.5053, 92.7808), + }, + "DCI-P3": { + "100% Red": (0.6800, 0.3200, 20.9492), + "100% Green": (0.2650, 0.6900, 72.1595), + "100% Blue": (0.1500, 0.0600, 6.8913), + "100% Cyan": (0.2048, 0.3602, 79.0508), + "100% Magenta": (0.3424, 0.1544, 27.8405), + "100% Yellow": (0.4248, 0.5476, 93.1087), + }, + "BT.2020": { + "100% Red": (0.7080, 0.2920, 26.2700), + "100% Green": (0.1700, 0.7970, 67.7998), + "100% Blue": (0.1310, 0.0460, 5.9302), + "100% Cyan": (0.1465, 0.3446, 73.7300), + "100% Magenta": (0.3682, 0.1471, 32.2002), + "100% Yellow": (0.4465, 0.5374, 94.0698), + }, +} +# BT.709 共用 sRGB 三原色,做别名指向,避免重复维护 +_PRIMARY_XYY_BY_GAMUT["BT.709"] = _PRIMARY_XYY_BY_GAMUT["sRGB"] + +# 当前激活色域(默认 sRGB),随信号格式切换调用 set_active_gamut() 修改 +_ACTIVE_GAMUT = "sRGB" + + +def set_active_gamut(gamut_name): + """ + 切换当前色域(随信号格式选择调用)。 + 支持: 'sRGB' / 'BT.709' / 'DCI-P3' / 'BT.2020'。 + """ + global _ACTIVE_GAMUT + if gamut_name not in _PRIMARY_XYY_BY_GAMUT: + raise ValueError( + f"未知色域: {gamut_name},可选: {list(_PRIMARY_XYY_BY_GAMUT)}" + ) + _ACTIVE_GAMUT = gamut_name + + +def get_active_gamut(): + """返回当前激活的色域名。""" + return _ACTIVE_GAMUT + + +def _current_primaries(): + """返回当前色域的原色表。""" + return _PRIMARY_XYY_BY_GAMUT[_ACTIVE_GAMUT] + + +# ====================================================================== +# 测试流程用名 -> 标准来源 +# 彩色块: 映射到 _COLOR_PATCH_XYY 的键 +# 灰阶 : 映射到 linear 信号值(Yr = 信号值×100) +# 100%原色: 走当前色域的 _PRIMARY_XYY_BY_GAMUT +# ====================================================================== +_PATCH_NAME_MAP = { + "Dark Skin": "dark skin", + "Light Skin": "light skin", + "Blue Sky": "blue sky", + "Foliage": "foliage", + "Blue Flower": "blue flower", + "Bluish Green": "bluish green", + "Orange": "orange", + "Purplish Blue": "purplish blue", + "Moderate Red": "moderate red", + "Purple": "purple", + "Yellow Green": "yellow green", + "Orange Yellow": "orange yellow", + "Blue (Legacy)": "blue", + "Green (Legacy)": "green", + "Red (Legacy)": "red", + "Yellow (Legacy)": "yellow", + "Magenta (Legacy)": "magenta", + "Cyan (Legacy)": "cyan", +} + +# 灰阶:信号值(线性域目标相对亮度),色度恒为 D65 白点 +_GRAYSCALE_SIGNAL = { + "White": 1.00, + "Gray 80": 0.80, + "Gray 65": 0.65, + "Gray 50": 0.50, + "Gray 35": 0.35, + "Black": 0.00, +} + + _SDR_COLOR_PATTERNS = [ ("White", 255, 255, 255), ("Gray 80", 230, 230, 230), @@ -81,57 +182,118 @@ _SDR_COLOR_PATTERNS = [ ] +# ---------------------------------------------------------------------- +# 标准 xy / Yr 解析(统一入口) +# ---------------------------------------------------------------------- def _resolve_reference_xy(name): - return _ACCURACY_REFERENCE_XY.get(name, (D65_X, D65_Y)) + """返回该色块的标准参考 xy。未知则回退 D65。""" + if name in _GRAYSCALE_SIGNAL: + return (D65_X, D65_Y) + if name in _PATCH_NAME_MAP: + x, y, _ = _COLOR_PATCH_XYY[_PATCH_NAME_MAP[name]] + return (x, y) + primaries = _current_primaries() + if name in primaries: + x, y, _ = primaries[name] + return (x, y) + return (D65_X, D65_Y) +def _resolve_reference_yr(name): + """ + 返回该色块的标准相对亮度因子 Yr (0~100)。 + 灰阶: 信号值×100 (linear)。彩色/原色: 光谱或色域 Yr。 + 无法确定返回 None。 + """ + if name in _GRAYSCALE_SIGNAL: + return _GRAYSCALE_SIGNAL[name] * 100.0 + if name in _PATCH_NAME_MAP: + return _COLOR_PATCH_XYY[_PATCH_NAME_MAP[name]][2] + primaries = _current_primaries() + if name in primaries: + return primaries[name][2] + return None + + +def get_target_xyY(name, white_lv): + """ + 返回该色块的完整 target (x, y, Y)。 + Target Y = white_lv × Yr / 100 (统一规则)。 + Yr 不可知时 Y 返回 None,由调用方按实测处理。 + """ + x, y = _resolve_reference_xy(name) + Yr = _resolve_reference_yr(name) + if Yr is None or white_lv is None or white_lv <= 0: + return (x, y, None) + return (x, y, round(white_lv * Yr / 100.0, 4)) + + +# ---------------------------------------------------------------------- +# 兼容旧接口 +# ---------------------------------------------------------------------- def get_accuracy_reference_y(name, white_lv): """ - 返回图表/表格用的参考亮度(Calman 目标 Y 比例,White=100 缩放)。 - - 注意:ΔE2000 计算使用实测 Y 作为目标 Y(与 Calman 一致),此函数仅供展示。 + 返回图表/表格用的参考亮度比例(White=100 缩放)。 + 现基于标准 Yr 返回真实比例;无标准 Yr 时回退 100。 """ - del name - if white_lv <= 0: + Yr = _resolve_reference_yr(name) + if Yr is None or white_lv is None or white_lv <= 0: return 100.0 - return white_lv + return round(Yr, 4) -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: +# ---------------------------------------------------------------------- +# xyY -> XYZ -> Lab(CIE 官方定义) +# ---------------------------------------------------------------------- +def _xyY_to_XYZ(x, y, Y): + """xyY 转 XYZ。y 为 0 时返回全 0,避免除零。""" + if y <= 0: return 0.0, 0.0, 0.0 + X = (x / y) * Y + Z = ((1.0 - x - y) / y) * Y + return X, Y, Z - X = x * Y / y - Z = (1 - x - y) * Y / y - Xn, Yn, Zn = 95.047, 100.000, 108.883 + +def _XYZ_to_lab(X, Y, Z, Xn, Yn, Zn): + """ + XYZ 转 CIE L*a*b*。 + Xn, Yn, Zn 为参考白点的绝对 XYZ(Yn 通常为白场亮度 white_lv)。 + """ + delta = 6.0 / 29.0 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 + return t / (3.0 * delta ** 2) + 4.0 / 29.0 + + xr = X / Xn if Xn != 0 else 0.0 + yr = Y / Yn if Yn != 0 else 0.0 + zr = Z / Zn if Zn != 0 else 0.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) + L = 116.0 * fy - 16.0 + a = 500.0 * (fx - fy) + b = 200.0 * (fy - fz) + return L, a, b -def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2): - L_bar = (L1 + L2) / 2.0 +def _xyY_to_lab(x, y, Y, white_x=D65_X, white_y=D65_Y, white_Y=None): + """ + xyY 直接转 Lab。 + 白点默认 D65 色度;white_Y 为白场绝对亮度(用于 L 的归一化基准)。 + 若 white_Y 为 None,则退化为以 Y=1 归一化(仅相对比较时可用)。 + """ + if white_Y is None or white_Y <= 0: + white_Y = 1.0 + X, Y3, Z = _xyY_to_XYZ(x, y, Y) + Xn, Yn, Zn = _xyY_to_XYZ(white_x, white_y, white_Y) + return _XYZ_to_lab(X, Y3, Z, Xn, Yn, Zn) + + +# ---------------------------------------------------------------------- +# CIEDE2000(官方公式) +# ---------------------------------------------------------------------- +def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2, kL=1.0, kC=1.0, kH=1.0): C1 = math.sqrt(a1 ** 2 + b1 ** 2) C2 = math.sqrt(a2 ** 2 + b2 ** 2) C_bar = (C1 + C2) / 2.0 @@ -143,49 +305,55 @@ def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2): 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 + return 0.0 + h = math.degrees(math.atan2(b, a_prime)) if h < 0: - h += 360 + h += 360.0 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 - + # ΔL', ΔC' delta_L_prime = L2 - L1 delta_C_prime = C2_prime - C1_prime + + # Δh'(官方三分支) + if C1_prime * C2_prime == 0: + delta_h_prime = 0.0 + else: + dh = h2_prime - h1_prime + if abs(dh) <= 180: + delta_h_prime = dh + elif dh > 180: + delta_h_prime = dh - 360.0 + else: + delta_h_prime = dh + 360.0 + delta_H_prime = ( - 2 + 2.0 * 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 + # 平均值 + L_bar_prime = (L1 + L2) / 2.0 + C_bar_prime = (C1_prime + C2_prime) / 2.0 + + # H_bar'(官方分支) + if C1_prime * C2_prime == 0: + H_bar_prime = h1_prime + h2_prime + else: + dh_abs = abs(h1_prime - h2_prime) + if dh_abs <= 180: + H_bar_prime = (h1_prime + h2_prime) / 2.0 + elif (h1_prime + h2_prime) < 360: + H_bar_prime = (h1_prime + h2_prime + 360.0) / 2.0 + else: + H_bar_prime = (h1_prime + h2_prime - 360.0) / 2.0 T = ( 1 @@ -195,13 +363,15 @@ def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2): - 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)) + delta_theta = 30 * math.exp(-(((H_bar_prime - 275) / 25.0) ** 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 + S_L = 1 + (0.015 * (L_bar_prime - 50) ** 2) / math.sqrt( + 20 + (L_bar_prime - 50) ** 2 + ) + S_C = 1 + 0.045 * C_bar_prime + S_H = 1 + 0.015 * C_bar_prime * T return math.sqrt( (delta_L_prime / (kL * S_L)) ** 2 @@ -218,6 +388,7 @@ def calculate_delta_e_2000( standard_x, standard_y, standard_lv=None, + white_lv=None, ): """ 计算 ΔE 2000 色差。 @@ -227,15 +398,19 @@ def calculate_delta_e_2000( measured_lv: 测量的亮度(cd/m²) standard_x, standard_y: 标准的 xy 坐标 standard_lv: 标准亮度(cd/m²);默认与 measured_lv 相同 + white_lv: 白场亮度(cd/m²),作为 Lab 的 L 归一化基准; + 默认取 measured_lv(仅当不传时退化为相对比较) Returns: float: ΔE 2000 色差值 """ if standard_lv is None: standard_lv = measured_lv + if white_lv is None: + white_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) + L1, a1, b1 = _xyY_to_lab(measured_x, measured_y, measured_lv, white_Y=white_lv) + L2, a2, b2 = _xyY_to_lab(standard_x, standard_y, standard_lv, white_Y=white_lv) return _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2) @@ -243,12 +418,11 @@ def calculate_accuracy_delta_e_2000( patch_name, measured_x, measured_y, measured_lv, white_lv ): """ - 色准测试专用 ΔE2000(Calman 对齐)。 - - Calman 在 ColorChecker 测试中对每块使用固定参考 xy, - 且目标 Y 取实测 Y(同亮度下比较色度差异)。 + 色准测试专用 ΔE2000 + 标准 xy 来自光谱积分/色域定义(随当前色域); + 目标 Y 取实测 Y(同亮度下比较色度差异)。 + L 的归一化基准使用白场亮度 white_lv。 """ - del white_lv standard_x, standard_y = _resolve_reference_xy(patch_name) return calculate_delta_e_2000( measured_x, @@ -256,17 +430,22 @@ def calculate_accuracy_delta_e_2000( measured_lv, standard_x, standard_y, - measured_lv, + standard_lv=measured_lv, + white_lv=white_lv, ) def calculate_color_accuracy(measured, standard): - """计算色差(简化版,欧氏距离 × 1000)""" + """计算色差(简化版,xy 欧氏距离 × 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 + + +def get_accuracy_color_standards(test_type=None): + """返回色准标准(patch 名称 -> 参考 xy,随当前色域)。""" + del test_type + return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS} \ No newline at end of file diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index 4ca3cb3..434f774 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -432,11 +432,18 @@ def _send_ld_pattern_async(self: "PQAutomationApp", image_builder, success_msg, # GUI 入口(绑定为 PQAutomationApp 方法) # -------------------------------------------------------------------------- def send_ld_window(self: "PQAutomationApp", percentage): - - FIXED_WINDOW_PERCENTAGE = 40 + """按预设按钮发送对应窗口大小,亮度取自窗口亮度输入框。""" try: - luminance_percent = float(percentage) + window_percentage = int(float(percentage)) + if window_percentage < 1 or window_percentage > 100: + raise ValueError + except Exception: + messagebox.showwarning("参数错误", "窗口范围应为 1-100") + return + + try: + luminance_percent = float(self.ld_window_luminance_var.get()) if luminance_percent < 1 or luminance_percent > 100: raise ValueError except Exception: @@ -450,30 +457,30 @@ def send_ld_window(self: "PQAutomationApp", percentage): window_level = int(round(luminance_percent / 100 * 255)) self.log_gui.log( - f"发送 {FIXED_WINDOW_PERCENTAGE}%窗口(亮度{luminance_percent:.0f}%)...", + f"发送 {window_percentage}%窗口(亮度{luminance_percent:.0f}%)...", level="info", ) _set_current_ld_pattern( self, "峰值亮度", - f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)", - FIXED_WINDOW_PERCENTAGE, + f"{window_percentage}%窗口({luminance_percent:.0f}%亮度)", + window_percentage, ) def builder(width, height): return _ensure_window_image( width, height, - FIXED_WINDOW_PERCENTAGE, + window_percentage, window_level, ) _send_ld_pattern_async( self, builder, - f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)已发送", - f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)发送失败", + f"{window_percentage}%窗口({luminance_percent:.0f}%亮度)已发送", + f"{window_percentage}%窗口({luminance_percent:.0f}%亮度)发送失败", ) diff --git a/app/ucd/device.py b/app/ucd/device.py index 04dfa1c..ad20b2b 100644 --- a/app/ucd/device.py +++ b/app/ucd/device.py @@ -156,7 +156,8 @@ class _UcdSdkBackend: return True except Exception as e: - self.hard_reset() + self.last_error = str(e) + self._force_cleanup() return False def close(self): @@ -173,10 +174,6 @@ class _UcdSdkBackend: log.exception("UcdSdk.close device object failed") finally: self._reset_state() - try: - self._reinit_tsi_lib() - except Exception: - log.exception("UcdSdk.close reinit failed") return True def hard_reset(self) -> None: diff --git a/app/views/chart_frame.py b/app/views/chart_frame.py index 82e3b5d..6e0b0f4 100644 --- a/app/views/chart_frame.py +++ b/app/views/chart_frame.py @@ -10,6 +10,7 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from app.views.pq_debug_panel import PQDebugPanel from app.views.modern_styles import get_theme_palette +from app.tests.color_accuracy import get_target_xyY from typing import TYPE_CHECKING @@ -755,11 +756,16 @@ def update_accuracy_result_table(self: "PQAutomationApp", accuracy_data, standar row_Y = ["Y"] row_tx = ["Target x:CIE31"] row_ty = ["Target y:CIE31"] + row_tY = ["Target Y"] row_de2000 = ["ΔE 2000"] include_itp = bool(delta_e_itp_values) row_deitp = ["ΔE ITP"] if include_itp else None + white_lv = None + if measurements and len(measurements[0]) >= 3: + white_lv = measurements[0][2] + for i, name in enumerate(color_patches): m = measurements[i] if i < len(measurements) else None sx, sy = standards.get(name, (None, None)) @@ -776,6 +782,12 @@ def update_accuracy_result_table(self: "PQAutomationApp", accuracy_data, standar row_tx.append(fmt(sx, 4)) row_ty.append(fmt(sy, 4)) + if white_lv is not None: + _, _, target_Y = get_target_xyY(name, white_lv) + row_tY.append(fmt(target_Y, 4) if target_Y is not None else "N/A") + else: + row_tY.append("N/A") + de = delta_e_values[i] if i < len(delta_e_values) else None row_de2000.append(fmt(de, 4)) @@ -783,7 +795,7 @@ def update_accuracy_result_table(self: "PQAutomationApp", accuracy_data, standar ditp = delta_e_itp_values[i] if i < len(delta_e_itp_values) else None row_deitp.append(fmt(ditp, 4)) - rows = [row_x, row_y, row_Y, row_tx, row_ty, row_de2000] + rows = [row_x, row_y, row_Y, row_tx, row_ty, row_tY, row_de2000] if include_itp and row_deitp is not None: rows.append(row_deitp) diff --git a/settings/pq_config.json b/settings/pq_config.json index c94d4f9..38ba782 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "local_dimming", + "current_test_type": "screen_module", "test_types": { "screen_module": { "name": "屏模组性能测试", @@ -54,7 +54,7 @@ "y_ideal": 0.329, "y_tolerance": 0.003 }, - "gamut_reference": "BT.709" + "gamut_reference": "BT.601" }, "hdr_movie": { "name": "HDR Movie测试", @@ -97,7 +97,7 @@ }, "device_config": { "ca_com": "COM3", - "ucd_list": "0: UCD-323 [2128C209]", + "ucd_list": "0: UCD-323 [2422C809]", "ca_channel": "0" }, "custom_pattern": {