根据新版本调整set_pattern:0~65535

This commit is contained in:
xinzhu.yin
2026-07-07 18:59:44 +08:00
parent 9fa811a9eb
commit 32aaef1c0f
24 changed files with 3016 additions and 279 deletions

View File

@@ -1,15 +1,17 @@
# -*- coding: UTF-8 -*-
"""
数据范围转换器
将 Full Range (0-255) 转换为 Limited Range (16-235)
使用方法:
from app.data_range_converter import DataRangeConverter
converter = DataRangeConverter()
converted_params = converter.convert(pattern_params, "Limited")
将 Full Range (065535) 转换为 Limited RangeSolidColor 刻度)
"""
from app.solid_color_scale import (
LIMITED_SOLID_COLOR_BLACK,
LIMITED_SOLID_COLOR_WHITE,
SOLID_COLOR_MAX,
SOLID_COLOR_MIN,
clamp_solid_color,
)
class DataRangeConverter:
"""数据范围转换器"""
@@ -25,38 +27,22 @@ class DataRangeConverter:
def convert_value(self, value):
"""
将单个 Full Range 值转换为 Limited Range
将单个 Full Range 值转换为 Limited RangeSolidColor 刻度)。
转换公式:
limited = 16 + (value / 255) × (235 - 16)
Args:
value: Full Range 值 (0-255)
Returns:
int: Limited Range 值 (16-235)
转换公式(归一化后线性映射):
limited = L_black + (value / MAX) × (L_white - L_black)
"""
# 边界值直接映射
if value == 0:
return 16
elif value == 255:
return 235
else:
# 线性映射
limited = 16 + round((value / 255.0) * (235 - 16))
# 限制范围
return max(16, min(235, limited))
value = clamp_solid_color(value)
if value <= SOLID_COLOR_MIN:
return LIMITED_SOLID_COLOR_BLACK
if value >= SOLID_COLOR_MAX:
return LIMITED_SOLID_COLOR_WHITE
span = LIMITED_SOLID_COLOR_WHITE - LIMITED_SOLID_COLOR_BLACK
limited = LIMITED_SOLID_COLOR_BLACK + round((value / SOLID_COLOR_MAX) * span)
return max(LIMITED_SOLID_COLOR_BLACK, min(LIMITED_SOLID_COLOR_WHITE, limited))
def convert_rgb(self, r, g, b):
"""
转换单个 RGB 值
Args:
r, g, b: Full Range RGB 值 (0-255)
Returns:
tuple: Limited Range RGB 值 (16-235)
"""
"""转换单个 RGB 值SolidColor Full → Limited"""
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
def convert(self, pattern_params, data_range="Full"):
@@ -73,7 +59,7 @@ class DataRangeConverter:
# Full Range 不需要转换
if data_range == "Full":
if self.verbose:
print("使用 Full Range (0-255),无需转换")
print(f"使用 Full Range (0{SOLID_COLOR_MAX}),无需转换")
return pattern_params
# Limited Range 需要转换
@@ -112,8 +98,11 @@ class DataRangeConverter:
def _print_header(self):
"""打印转换头部信息"""
print("=" * 80)
print("【数据范围转换】Limited Range (16-235)")
print(" 转换公式: 16 + (value / 255) × (235 - 16)")
print("【数据范围转换】Limited Range (SolidColor 刻度)")
print(
f" 映射: {SOLID_COLOR_MIN}{SOLID_COLOR_MAX}"
f"{LIMITED_SOLID_COLOR_BLACK}{LIMITED_SOLID_COLOR_WHITE}"
)
print("=" * 80)
def _print_conversion(self, index, r_orig, g_orig, b_orig, r_new, g_new, b_new):
@@ -126,25 +115,23 @@ class DataRangeConverter:
"""
# 判断是否需要打印
should_print = False
label = ""
mid = SOLID_COLOR_MAX // 2
# 第一个和最后一个
if index == 0:
should_print = True
label = "黑色"
elif index == len([]) - 1: # 需要在外部判断
should_print = True
label = "白色"
# 关键 RGB 值
elif (
r_orig in [0, 128, 255]
or g_orig in [0, 128, 255]
or b_orig in [0, 128, 255]
r_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
or g_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
or b_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
):
should_print = True
if r_orig == 128:
if r_orig == mid:
label = "50%"
else:
label = ""
if should_print:
diff = abs(r_new - r_orig)
@@ -165,9 +152,9 @@ class DataRangeConverter:
return {
"name": "Data Range Converter",
"version": "1.0.0",
"full_range": "0-255",
"limited_range": "16-235",
"formula": "16 + (value / 255) × 219",
"full_range": f"0-{SOLID_COLOR_MAX}",
"limited_range": f"{LIMITED_SOLID_COLOR_BLACK}-{LIMITED_SOLID_COLOR_WHITE}",
"formula": "L_black + (value / MAX) × (L_white - L_black)",
}
@@ -188,9 +175,9 @@ def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
示例:
>>> from app.data_range_converter import convert_pattern_params
>>> params = [[0,0,0], [255,255,255]]
>>> params = [[0,0,0], [65535,65535,65535]]
>>> converted = convert_pattern_params(params, "Limited")
[[16,16,16], [235,235,235]]
[[4112,4112,4112], [60492,60492,60492]]
"""
converter = DataRangeConverter(verbose=verbose)
return converter.convert(pattern_params, data_range)
@@ -201,7 +188,7 @@ def convert_single_rgb(r, g, b, data_range="Full"):
便捷函数:转换单个 RGB 值
Args:
r, g, b: RGB 值 (0-255)
r, g, b: RGB 值 (065535)
data_range: "Full""Limited"
Returns:
@@ -232,7 +219,8 @@ if __name__ == "__main__":
print("\n[测试 1] 基本转换...")
converter = DataRangeConverter(verbose=False)
test_values = [0, 16, 64, 128, 192, 235, 255]
test_values = [0, LIMITED_SOLID_COLOR_BLACK, SOLID_COLOR_MAX // 4, SOLID_COLOR_MAX // 2,
SOLID_COLOR_MAX * 3 // 4, LIMITED_SOLID_COLOR_WHITE, SOLID_COLOR_MAX]
print(" Full Range → Limited Range:")
for v in test_values:
limited = converter.convert_value(v)
@@ -243,8 +231,8 @@ if __name__ == "__main__":
print("\n[测试 2] RGB 转换...")
test_rgb = [
(0, 0, 0),
(128, 128, 128),
(255, 255, 255),
(SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2),
(SOLID_COLOR_MAX, SOLID_COLOR_MAX, SOLID_COLOR_MAX),
]
for r, g, b in test_rgb:
@@ -253,12 +241,14 @@ if __name__ == "__main__":
# 测试 3: 完整转换流程
print("\n[测试 3] 完整转换流程...")
from app.ucd.solid_color_scale import byte_to_solid_color as _sc
pattern_params = [
[255, 255, 255], # 100% 白
[230, 230, 230], # 90%
[204, 204, 204], # 80%
[128, 128, 128], # 50%
[0, 0, 0], # 0% 黑
[_sc(255)] * 3,
[_sc(230)] * 3,
[_sc(204)] * 3,
[_sc(128)] * 3,
[0, 0, 0],
]
converted = converter.convert(pattern_params, "Limited")
@@ -270,11 +260,11 @@ if __name__ == "__main__":
# 测试 4: 便捷函数
print("\n[测试 4] 便捷函数...")
result = convert_pattern_params(
[[0, 0, 0], [255, 255, 255]], "Limited", verbose=False
[[0, 0, 0], [SOLID_COLOR_MAX, SOLID_COLOR_MAX, SOLID_COLOR_MAX]], "Limited", verbose=False
)
print(f" 结果: {result}")
r, g, b = convert_single_rgb(128, 128, 128, "Limited")
r, g, b = convert_single_rgb(SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, "Limited")
print(f" RGB(128,128,128) → RGB({r},{g},{b})")
# 测试 5: 获取信息

View File

@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
"""
import numpy as np
from app.solid_color_scale import solid_color_to_pct
from app.views.modern_styles import get_theme_palette
from typing import TYPE_CHECKING
@@ -25,7 +26,14 @@ def _is_dark_palette(palette: dict[str, str]) -> bool:
def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_gamma, test_type):
def plot_gamma(
self: "PQAutomationApp",
L_bar,
results_with_gamma_list,
target_gamma,
test_type,
pattern_params=None,
):
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
palette = get_theme_palette()
dark_mode = _is_dark_palette(palette)
@@ -51,13 +59,19 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
for spine in self.gamma_ax.spines.values():
spine.set_color(palette["border"])
# 生成横坐标(灰阶百分比)
x_values = np.linspace(0, 100, len(L_bar))
# 横坐标:优先用 pattern 码值换算的灰阶百分比,否则等分
if pattern_params and len(pattern_params) == len(L_bar):
x_values = np.array([
solid_color_to_pct(row[0]) for row in pattern_params
])
else:
x_values = np.linspace(0, 100, len(L_bar))
# 反转 L_bar确保从左到右是 0% → 100%
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
L_bar = L_bar[::-1]
results_with_gamma_list = results_with_gamma_list[::-1]
x_values = x_values[::-1]
# 计算平均Gamma
gamma_values = []

View File

@@ -3,6 +3,13 @@ import json
import copy
from pathlib import Path
from app.solid_color_scale import (
SOLID_COLOR_MAX,
ensure_solid_color_pattern,
pct_to_solid_color,
solid_color_rgb as _rgb,
)
# =============================================================================
# Pattern 文件读写工具统一存储格式settings/patterns/{name}.json
@@ -25,7 +32,7 @@ def load_pattern_file(filepath) -> dict:
}
"""
with open(filepath, encoding="utf-8") as f:
return json.load(f)
return ensure_solid_color_pattern(json.load(f))
def save_pattern_file(filepath, pattern: dict) -> None:
@@ -137,9 +144,9 @@ _PATTERN_RGB = {
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[255, 0, 0], # 红色
[0, 255, 0], # 绿色
[0, 0, 255], # 蓝色
_rgb(255, 0, 0), # 红色
_rgb(0, 255, 0), # 绿色
_rgb(0, 0, 255), # 蓝色
],
}
@@ -149,17 +156,17 @@ _PATTERN_GRAY_FALLBACK = {
"measurement_bit_depth": 8,
"measurement_max_value": 10,
"pattern_params": [
[255, 255, 255], # 100% 白色
[230, 230, 230], # 90%
[205, 205, 205], # 80%
[179, 179, 179], # 70%
[154, 154, 154], # 60%
[128, 128, 128], # 50%
[102, 102, 102], # 40%
[78, 78, 78], # 30%
[52, 52, 52], # 20%
[26, 26, 26], # 10%
[0, 0, 0], # 0% 黑色
_rgb(255, 255, 255), # 100% 白色
_rgb(230, 230, 230), # 90%
_rgb(205, 205, 205), # 80%
_rgb(179, 179, 179), # 70%
_rgb(154, 154, 154), # 60%
_rgb(128, 128, 128), # 50%
_rgb(102, 102, 102), # 40%
_rgb(78, 78, 78), # 30%
_rgb(52, 52, 52), # 20%
_rgb(26, 26, 26), # 10%
_rgb(0, 0, 0), # 0% 黑色
],
}
# 灰阶 pattern 从文件加载,支持用户编辑;文件缺失时回退到硬编码兜底
@@ -171,37 +178,37 @@ _PATTERN_ACCURACY = {
"measurement_max_value": 28, # 29个颜色最大索引是28
"pattern_params": [
# ========== 灰阶 (5个) ==========
[255, 255, 255], # 0: White
[230, 230, 230], # 1: Gray 80
[209, 209, 209], # 2: Gray 65
[186, 186, 186], # 3: Gray 50
[158, 158, 158], # 4: Gray 35
_rgb(255, 255, 255), # 0: White
_rgb(230, 230, 230), # 1: Gray 80
_rgb(209, 209, 209), # 2: Gray 65
_rgb(186, 186, 186), # 3: Gray 50
_rgb(158, 158, 158), # 4: Gray 35
# ========== ColorChecker 24色 (18个) ==========
[115, 82, 66], # 5: Dark Skin
[194, 150, 130], # 6: Light Skin
[94, 122, 156], # 7: Blue Sky
[89, 107, 66], # 8: Foliage
[130, 128, 176], # 9: Blue Flower
[99, 189, 168], # 10: Bluish Green
[217, 120, 41], # 11: Orange
[74, 92, 163], # 12: Purplish Blue
[194, 84, 97], # 13: Moderate Red
[92, 61, 107], # 14: Purple
[158, 186, 64], # 15: Yellow Green
[230, 161, 46], # 16: Orange Yellow
[51, 61, 150], # 17: Blue (Legacy)
[71, 148, 71], # 18: Green (Legacy)
[176, 48, 59], # 19: Red (Legacy)
[237, 199, 33], # 20: Yellow (Legacy)
[186, 84, 145], # 21: Magenta (Legacy)
[0, 133, 163], # 22: Cyan (Legacy)
_rgb(115, 82, 66), # 5: Dark Skin
_rgb(194, 150, 130), # 6: Light Skin
_rgb(94, 122, 156), # 7: Blue Sky
_rgb(89, 107, 66), # 8: Foliage
_rgb(130, 128, 176), # 9: Blue Flower
_rgb(99, 189, 168), # 10: Bluish Green
_rgb(217, 120, 41), # 11: Orange
_rgb(74, 92, 163), # 12: Purplish Blue
_rgb(194, 84, 97), # 13: Moderate Red
_rgb(92, 61, 107), # 14: Purple
_rgb(158, 186, 64), # 15: Yellow Green
_rgb(230, 161, 46), # 16: Orange Yellow
_rgb(51, 61, 150), # 17: Blue (Legacy)
_rgb(71, 148, 71), # 18: Green (Legacy)
_rgb(176, 48, 59), # 19: Red (Legacy)
_rgb(237, 199, 33), # 20: Yellow (Legacy)
_rgb(186, 84, 145), # 21: Magenta (Legacy)
_rgb(0, 133, 163), # 22: Cyan (Legacy)
# ========== 100% 饱和色 (6个) ==========
[255, 0, 0], # 23: 100% Red
[0, 255, 0], # 24: 100% Green
[0, 0, 255], # 25: 100% Blue
[0, 255, 255], # 26: 100% Cyan
[255, 0, 255], # 27: 100% Magenta
[255, 255, 0], # 28: 100% Yellow
_rgb(255, 0, 0), # 23: 100% Red
_rgb(0, 255, 0), # 24: 100% Green
_rgb(0, 0, 255), # 25: 100% Blue
_rgb(0, 255, 255), # 26: 100% Cyan
_rgb(255, 0, 255), # 27: 100% Magenta
_rgb(255, 255, 0), # 28: 100% Yellow
],
}
@@ -505,7 +512,7 @@ def _gen_even_gray(n: int) -> list[list[int]]:
out = []
for i in range(n):
pct = 100.0 - (100.0 / (n - 1)) * i
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
out.append([v, v, v])
return out
@@ -521,7 +528,7 @@ def _gen_pq_gray(n: int) -> list[list[int]]:
out = []
for i in range(n):
v_pq = 1.0 - i / (n - 1)
v = int(round(v_pq * 255))
v = int(round(v_pq * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -534,7 +541,7 @@ def _gen_gamma_gray(n: int, gamma: float = 2.2) -> list[list[int]]:
for i in range(n):
lin = 1.0 - i / (n - 1) # 线性光 1→0
code = lin ** (1.0 / gamma) # gamma 编码
v = int(round(code * 255))
v = int(round(code * SOLID_COLOR_MAX))
out.append([v, v, v])
return out

View File

@@ -51,12 +51,13 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
, level="info")
# 根据测试类型执行不同的测试流程
ok = True
if test_type == "screen_module":
self.run_screen_module_test(test_items)
ok = self.run_screen_module_test(test_items)
elif test_type == "sdr_movie":
self.run_sdr_movie_test(test_items)
ok = self.run_sdr_movie_test(test_items)
elif test_type == "hdr_movie":
self.run_hdr_movie_test(test_items)
ok = self.run_hdr_movie_test(test_items)
elif test_type == "local_dimming":
self.log_gui.log(
"Local Dimming 为手动模式,请在 Local Dimming 面板发送图案并采集亮度",
@@ -65,7 +66,10 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
# 测试完成后更新UI状态
if self.testing: # 如果没有被中途停止
self._dispatch_ui(self.on_test_completed)
if ok:
self._dispatch_ui(self.on_test_completed)
else:
self._dispatch_ui(self.on_test_error)
except Exception as e:
self.log_gui.log(f"测试过程中发生错误: {str(e)}", level="info")
import traceback
@@ -82,7 +86,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
self.new_pq_results("screen_module", "屏模组性能测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 判断是否需要灰阶数据
needs_gray_data = any(
@@ -96,7 +100,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing: # 检查是否被停止
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -120,7 +124,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足,跳过相关测试", level="error")
return
return False
self.log_gui.log(
f"灰阶数据采集完成,共 {len(shared_gray_data)} 个数据点"
@@ -148,6 +152,8 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
elif item == "contrast":
self.test_contrast("screen_module", shared_gray_data)
return True
def run_custom_sdr_test(self: "PQAutomationApp", test_items):
"""执行客户定制 SDR 测试 - 升级版"""
@@ -173,7 +179,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
self.new_pq_results("sdr_movie", "SDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
@@ -194,7 +200,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing:
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -212,7 +218,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
return
return False
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
@@ -235,6 +241,8 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
elif item == "accuracy":
self.test_color_accuracy("sdr_movie")
return True
def run_hdr_movie_test(self: "PQAutomationApp", test_items):
"""执行HDR Movie测试"""
@@ -244,7 +252,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
self.new_pq_results("hdr_movie", "HDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目", level="info")
return
return False
# 获取信号格式设置
color_space = self.hdr_color_space_var.get()
@@ -269,7 +277,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
for item in test_items:
if not self.testing:
return
return False
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
@@ -287,7 +295,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
return
return False
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
@@ -310,6 +318,8 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
elif item == "accuracy":
self.test_color_accuracy("hdr_movie")
return True
def send_fix_pattern(self: "PQAutomationApp", mode):
"""发送固定图案并采集数据 - 支持不同测试类型的信号格式"""
@@ -693,9 +703,9 @@ def test_gamma(self: "PQAutomationApp", test_type, gray_data=None):
# ========================================================
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
pattern_params = self.config.current_pattern.get(
"pattern_params", None
)
) or self.config.default_pattern_gray.get("pattern_params", None)
# 计算Gamma值使用修正后的 max_index_fix 和 8bit pattern参数
results_with_gamma_list, L_bar = self.calculate_gamma(
@@ -713,8 +723,12 @@ def test_gamma(self: "PQAutomationApp", test_type, gray_data=None):
target_gamma = 2.2
else:
target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type))
self.plot_gamma(
L_bar, results_with_gamma_list, target_gamma, test_type, pattern_params
)
self._save_chart_snapshot(
test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type)
)
self.log_gui.log("Gamma测试完成", level="success")
@@ -780,9 +794,9 @@ def test_eotf(self: "PQAutomationApp", test_type, gray_data=None):
self.log_gui.log(f"最终使用的 max_index_fix = {max_index_fix}", level="info")
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
pattern_params = self.config.current_pattern.get(
"pattern_params", None
)
) or self.config.default_pattern_gray.get("pattern_params", None)
# ========== 计算 EOTF复用 Gamma 计算逻辑使用8bit pattern参数==========
results_with_eotf_list, L_bar = self.calculate_gamma(

118
app/solid_color_scale.py Normal file
View File

@@ -0,0 +1,118 @@
"""UCD SolidColor 码值刻度(与 UniTAP ``SolidColorParams`` 一致065535"""
from __future__ import annotations
SOLID_COLOR_MIN = 0
SOLID_COLOR_MAX = 65535
# 官方 work_with_solid_color.py65535 刻度需 bpc>=10
SOLID_COLOR_MIN_BPC = 10
# Limited Range 在 SolidColor 刻度下的端点(等价于 8bit 的 16 / 235
_LIMITED_8BIT_BLACK = 16
_LIMITED_8BIT_WHITE = 235
LIMITED_SOLID_COLOR_BLACK = round(_LIMITED_8BIT_BLACK / 255 * SOLID_COLOR_MAX)
LIMITED_SOLID_COLOR_WHITE = round(_LIMITED_8BIT_WHITE / 255 * SOLID_COLOR_MAX)
def clamp_solid_color(value: int | float) -> int:
"""将码值限制在 SolidColor 合法范围内。"""
return max(SOLID_COLOR_MIN, min(SOLID_COLOR_MAX, int(round(value))))
def pct_to_solid_color(pct: float) -> int:
"""灰阶/亮度百分比 (0100) → SolidColor 码值。"""
pct = max(0.0, min(100.0, float(pct)))
return int(round(pct / 100.0 * SOLID_COLOR_MAX))
def solid_color_to_pct(value: int | float) -> float:
"""SolidColor 码值 → 百分比 (0100)。"""
if SOLID_COLOR_MAX <= 0:
return 0.0
return clamp_solid_color(value) / SOLID_COLOR_MAX * 100.0
def solid_color_to_byte(value: int | float) -> int:
"""SolidColor 码值 → 8bitUI 色块 / HEX 显示用)。"""
return int(round(clamp_solid_color(value) / SOLID_COLOR_MAX * 255))
def byte_to_solid_color(value: int | float) -> int:
"""旧 8bit 码值 (0255) → SolidColor 刻度。"""
byte = max(0, min(255, int(round(value))))
return int(round(byte / 255.0 * SOLID_COLOR_MAX))
def solid_color_rgb(r: int, g: int, b: int) -> list[int]:
"""将旧 8bit RGB 三元组转为 SolidColor 刻度。"""
return [byte_to_solid_color(r), byte_to_solid_color(g), byte_to_solid_color(b)]
def is_legacy_8bit_pattern_params(params: list) -> bool:
"""判断 ``pattern_params`` 是否仍为旧 0255 刻度。"""
if not params:
return False
max_val = 0
for row in params:
if not row:
continue
for channel in row[:3]:
try:
max_val = max(max_val, int(channel))
except (TypeError, ValueError):
pass
return max_val <= 255
def migrate_pattern_params(params: list) -> list:
"""将 0255 刻度的 pattern 参数迁移到 SolidColor 刻度。"""
if not is_legacy_8bit_pattern_params(params):
return params
migrated: list = []
for row in params:
head = [byte_to_solid_color(v) for v in row[:3]]
migrated.append(head + list(row[3:]))
return migrated
def ensure_solid_color_pattern(pattern: dict) -> dict:
"""确保 pattern 字典中的 ``pattern_params`` 使用 SolidColor 刻度。"""
params = pattern.get("pattern_params")
if params:
pattern["pattern_params"] = migrate_pattern_params(params)
return pattern
def pack_solid_color_hw_registers(
rgb: tuple[int, int, int] | list[int],
) -> tuple[int, int]:
"""将 65535 刻度 RGB 打包为 PG 硬件寄存器值(``shifter=0``,新版 UCD API
旧版 UniTAP SDK 在 ``set_pattern_params`` 内还会按 ``16-bpc`` 左移;
65535 刻度下左移会导致 32 位溢出30%/50%/80% 灰阶发暗)。
新版 UCD 直接接受 065535须绕过 SDK 移位。
"""
r = clamp_solid_color(rgb[0])
g = clamp_solid_color(rgb[1])
b = clamp_solid_color(rgb[2])
param0 = ((r << 16) | g) & 0xFFFFFFFF
param1 = b & 0xFFFFFFFF
return param0, param1
__all__ = [
"SOLID_COLOR_MIN",
"SOLID_COLOR_MAX",
"SOLID_COLOR_MIN_BPC",
"LIMITED_SOLID_COLOR_BLACK",
"LIMITED_SOLID_COLOR_WHITE",
"clamp_solid_color",
"pct_to_solid_color",
"solid_color_to_pct",
"solid_color_to_byte",
"byte_to_solid_color",
"solid_color_rgb",
"is_legacy_8bit_pattern_params",
"migrate_pattern_params",
"ensure_solid_color_pattern",
"pack_solid_color_hw_registers",
]

View File

@@ -18,6 +18,8 @@
import math
import numpy as np
from app.solid_color_scale import solid_color_rgb as _rgb
D65_X = 0.3127
D65_Y = 0.3290
@@ -150,35 +152,35 @@ _GRAYSCALE_SIGNAL = {
_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),
("White", *_rgb(255, 255, 255)),
("Gray 80", *_rgb(230, 230, 230)),
("Gray 65", *_rgb(209, 209, 209)),
("Gray 50", *_rgb(186, 186, 186)),
("Gray 35", *_rgb(158, 158, 158)),
("Dark Skin", *_rgb(115, 82, 66)),
("Light Skin", *_rgb(194, 150, 130)),
("Blue Sky", *_rgb(94, 122, 156)),
("Foliage", *_rgb(89, 107, 66)),
("Blue Flower", *_rgb(130, 128, 176)),
("Bluish Green", *_rgb(99, 189, 168)),
("Orange", *_rgb(217, 120, 41)),
("Purplish Blue", *_rgb(74, 92, 163)),
("Moderate Red", *_rgb(194, 84, 97)),
("Purple", *_rgb(92, 61, 107)),
("Yellow Green", *_rgb(158, 186, 64)),
("Orange Yellow", *_rgb(230, 161, 46)),
("Blue (Legacy)", *_rgb(51, 61, 150)),
("Green (Legacy)", *_rgb(71, 148, 71)),
("Red (Legacy)", *_rgb(176, 48, 59)),
("Yellow (Legacy)", *_rgb(237, 199, 33)),
("Magenta (Legacy)", *_rgb(186, 84, 145)),
("Cyan (Legacy)", *_rgb(0, 133, 163)),
("100% Red", *_rgb(255, 0, 0)),
("100% Green", *_rgb(0, 255, 0)),
("100% Blue", *_rgb(0, 0, 255)),
("100% Cyan", *_rgb(0, 255, 255)),
("100% Magenta", *_rgb(255, 0, 255)),
("100% Yellow", *_rgb(255, 255, 0)),
]

View File

@@ -13,6 +13,20 @@ from app.ucd.device import (
list_devices,
)
from app.ucd.service import PatternService, PatternSession, SignalService
from app.solid_color_scale import (
SOLID_COLOR_MAX,
SOLID_COLOR_MIN,
SOLID_COLOR_MIN_BPC,
byte_to_solid_color,
clamp_solid_color,
ensure_solid_color_pattern,
migrate_pattern_params,
pack_solid_color_hw_registers,
pct_to_solid_color,
solid_color_rgb,
solid_color_to_byte,
solid_color_to_pct,
)
__all__ = [
"SignalService",
@@ -25,4 +39,15 @@ __all__ = [
"EventBus",
"ConnectionChanged",
"DeviceKind",
"SOLID_COLOR_MIN",
"SOLID_COLOR_MAX",
"SOLID_COLOR_MIN_BPC",
"clamp_solid_color",
"pct_to_solid_color",
"solid_color_to_pct",
"solid_color_to_byte",
"byte_to_solid_color",
"solid_color_rgb",
"migrate_pattern_params",
"ensure_solid_color_pattern",
]

View File

@@ -43,6 +43,8 @@ from app.ucd.domain import (
is_ycbcr,
)
from app.ucd.enum import UCDEnum
from app.ucd.pg_status import read_pattern_generator_status_lines
from app.solid_color_scale import SOLID_COLOR_MIN_BPC, clamp_solid_color, pack_solid_color_hw_registers
log = logging.getLogger(__name__)
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
@@ -108,6 +110,7 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.last_error = None
self._solid_color_stream_ready = False
def search_device(self):
"""搜索可用设备"""
@@ -213,6 +216,10 @@ class _UcdSdkBackend:
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._solid_color_stream_ready = False
def _invalidate_solid_color_stream(self) -> None:
self._solid_color_stream_ready = False
def _force_cleanup(self):
"""强制清理所有状态"""
@@ -440,6 +447,7 @@ class _UcdSdkBackend:
pg, _ = self.get_tx_modules()
log.info("UcdSdk.set_video_mode calling pg.set_vm()")
pg.set_vm(vm=video_mode)
self._invalidate_solid_color_stream()
self._stop_audio_output()
log.info("UcdSdk.set_video_mode done")
self._last_sent_config = current_config
@@ -681,6 +689,104 @@ class _UcdSdkBackend:
log.warning("UcdSdk.set_pattern_params unsupported pattern=%s", getattr(pattern, "name", pattern))
return False
def _solid_color_pattern_ids(self):
return {
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
UCDEnum.VideoPatternInfo.VideoPattern.SolidColor,
}
def _is_solid_color_pattern(self, pattern) -> bool:
return pattern in self._solid_color_pattern_ids()
def _write_pg_solid_color_registers(self, pg, rgb_list) -> None:
"""直接写入 SolidColor 硬件寄存器65535 刻度,无 SDK 左移)。"""
from ctypes import c_uint
from UniTAP.libs.lib_tsi import ci
param0, param1 = pack_solid_color_hw_registers(rgb_list)
pg._become_active()
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_1, param0, c_uint)
pg._PatternGenerator__io.set(ci.TSI_PG_PREDEF_PATTERN_PARAM_2, param1, c_uint)
log.info(
"UcdSdk._write_pg_solid_color_registers rgb=%s hw=(0x%08X, 0x%08X)",
rgb_list,
param0,
param1,
)
def apply_solid_color(self, rgb, *, force_reapply: bool = False) -> bool:
"""发送 SolidColor顺序对齐 ``work_with_solid_color.py``。
首次(或信号格式变化后):``set_pattern → set_vm → apply() → set_pattern_params``。
同一会话内后续仅 ``set_pattern_params``,避免重复 ``apply()`` 导致 HDMI 重锁、
灰阶测量出现锯齿形 Gamma 曲线。
"""
if not self.status or not self.role:
return False
try:
pg, _ = self.get_tx_modules()
pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if pattern is None:
log.error("UcdSdk.apply_solid_color failed: solidcolor pattern not found")
return False
rgb_list = [clamp_solid_color(v) for v in list(rgb)[:3]]
if len(rgb_list) < 3:
log.error("UcdSdk.apply_solid_color invalid rgb=%s", rgb)
return False
if self.current_timing is None:
self.current_timing = self._resolve_timing(pg)
if self.current_timing is None:
log.error("UcdSdk.apply_solid_color current_timing is None")
self.last_error = "current_timing is None"
return False
bpc_bumped = False
if self.color_info.bpc < SOLID_COLOR_MIN_BPC:
log.info(
"UcdSdk.apply_solid_color bump bpc %s -> %s for SolidColor video mode",
self.color_info.bpc,
SOLID_COLOR_MIN_BPC,
)
self.color_info.bpc = SOLID_COLOR_MIN_BPC
bpc_bumped = True
self.current_pattern = pattern
self.current_pattern_param = UniTAP.SolidColorParams(
first=rgb_list[0],
second=rgb_list[1],
third=rgb_list[2],
)
need_full_setup = (
force_reapply
or bpc_bumped
or not self._solid_color_stream_ready
)
if need_full_setup:
log.info("UcdSdk.apply_solid_color full setup rgb=%s", rgb_list)
pg.set_pattern(pattern=pattern)
self.set_video_mode()
if not self._apply_pg_output(pg):
log.error("UcdSdk.apply_solid_color pg.apply failed")
self._invalidate_solid_color_stream()
return False
self._solid_color_stream_ready = True
else:
log.info("UcdSdk.apply_solid_color params-only rgb=%s", rgb_list)
self._write_pg_solid_color_registers(pg, rgb_list)
log.info("UcdSdk.apply_solid_color done")
return True
except Exception:
log.exception("UcdSdk.apply_solid_color exception")
self._invalidate_solid_color_stream()
return False
def apply_pattern(self):
"""应用当前pattern"""
if self.current_pattern is not None:
@@ -715,6 +821,7 @@ class _UcdSdkBackend:
def set_ucd_params(self, config):
"""设置UCD323参数"""
self._invalidate_solid_color_stream()
self.last_error = None
self.config = config
test_type = self.config.current_test_type
@@ -799,6 +906,10 @@ class _UcdSdkBackend:
log.error("UcdSdk.send_current_pattern_params failed: set_pattern returned False")
return False
if self._is_solid_color_pattern(self.current_pattern):
log.info("UcdSdk.send_current_pattern_params using apply_solid_color")
return self.apply_solid_color(pattern_params)
log.info("UcdSdk.send_current_pattern_params calling run()")
self.run()
log.info("UcdSdk.send_current_pattern_params done")
@@ -814,12 +925,7 @@ class _UcdSdkBackend:
try:
log.info("UcdSdk.send_solid_rgb_pattern rgb=%s", rgb)
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if self.current_pattern is None:
log.error("UcdSdk.send_solid_rgb_pattern failed: solidcolor pattern not found")
return False
return self.send_current_pattern_params(list(rgb))
return self.apply_solid_color(list(rgb))
except Exception:
log.exception("UcdSdk.send_solid_rgb_pattern exception")
return False
@@ -839,6 +945,20 @@ class _UcdSdkBackend:
except Exception:
return False
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
"""读取 PG 当前状态,格式对齐 UCD Console 的 Pattern Generator Status。"""
if not self.status or not self.role:
return []
try:
pg, _ = self.get_tx_modules()
return read_pattern_generator_status_lines(pg, pattern_hint=pattern_hint)
except Exception:
log.exception("UcdSdk.get_pattern_generator_status_lines failed")
return []
# ─── §3 DeviceInfo / list_devices ────────────────────────────────
@@ -1244,6 +1364,13 @@ class UCD323Device(IUcdDevice):
self._state = UcdState.APPLIED
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
with self._acquire_device_lock("get_pattern_generator_status_lines"):
return self._sdk.get_pattern_generator_status_lines(pattern_hint=pattern_hint)
# --- 内部 ---
def _apply_dynamic_range(self, signal: SignalFormat) -> None:
@@ -1262,6 +1389,13 @@ class UCD323Device(IUcdDevice):
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
return bool(self._sdk.send_image_pattern(pattern.image_path))
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
if not self._sdk.apply_solid_color(list(pattern.solid_rgb)):
raise UcdApplyFailed("apply_solid_color 返回 False")
return True
# 预定义图案路径:复用 controller.set_pattern + run()
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
@@ -1270,11 +1404,7 @@ class UCD323Device(IUcdDevice):
self._sdk.current_pattern = video_pattern
params: list[int] | None = None
if pattern.kind is PatternKind.SOLID:
if pattern.solid_rgb is None:
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
params = list(pattern.solid_rgb)
elif pattern.extras:
if pattern.extras:
params = list(pattern.extras)
if not self._sdk.set_pattern(video_pattern, params):

111
app/ucd/pg_status.py Normal file
View File

@@ -0,0 +1,111 @@
"""从 UniTAP Pattern Generator 读取状态并格式化为日志行。"""
from __future__ import annotations
import logging
import os
import re
from typing import Any
import UniTAP
from UniTAP.common import ColorInfo
log = logging.getLogger(__name__)
def _enum_display_name(name: str) -> str:
spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
return spaced.replace("_", " ")
def _format_pattern_name(pg, pattern_hint: str | None = None) -> str:
if pattern_hint:
return pattern_hint
pattern = getattr(pg, "_pattern", None)
if isinstance(pattern, UniTAP.VideoPattern):
return _enum_display_name(pattern.name)
if isinstance(pattern, str):
return os.path.basename(pattern) or "Image File"
pattern_id = getattr(pg, "_pattern_id", None)
if pattern_id is not None:
return _enum_display_name(getattr(pattern_id, "name", str(pattern_id)))
return ""
def _format_cef(ci: ColorInfo) -> str:
fmt_map = {
ColorInfo.ColorFormat.CF_RGB: "RGB",
ColorInfo.ColorFormat.CF_YCbCr_422: "YCbCr 4:2:2",
ColorInfo.ColorFormat.CF_YCbCr_444: "YCbCr 4:4:4",
ColorInfo.ColorFormat.CF_YCbCr_420: "YCbCr 4:2:0",
ColorInfo.ColorFormat.CF_Y_ONLY: "Y only",
ColorInfo.ColorFormat.CF_RAW: "RAW",
ColorInfo.ColorFormat.CF_DSC: "DSC",
}
fmt = fmt_map.get(
ci.color_format,
ci.color_format.name.replace("CF_", "").replace("_", " "),
)
if ci.color_format == ColorInfo.ColorFormat.CF_RGB:
range_str = (
"Legacy RGB mode"
if ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA
else "Limited range"
)
elif ci.dynamic_range == ColorInfo.DynamicRange.DR_VESA:
range_str = "Full range"
else:
range_str = "Limited range"
return f"{fmt}/{range_str}"
def _max_bitrate_gbps(pg, bpp: int) -> float:
caps = pg._caps()
effective_bpp = bpp if bpp > 0 else 24
return round(caps.max_pix_rate * effective_bpp / 1_000_000_000.0, 3)
def collect_pattern_generator_status(
pg,
*,
pattern_hint: str | None = None,
) -> dict[str, Any]:
vm = pg.get_stream_video_mode()
pg_status = pg.status()
ci = vm.color_info
refresh_hz = vm.timing.frame_rate / 1000.0 if vm.timing.frame_rate else 0.0
timing = f"{vm.timing.hactive} x {vm.timing.vactive} @ {refresh_hz:g}Hz"
current_br = vm.bit_rate
max_br = _max_bitrate_gbps(pg, ci.bpp)
return {
"total_bitrate": f"{current_br:.3f} / {max_br:.3f} Gbps",
"timing": timing,
"pattern": _format_pattern_name(pg, pattern_hint),
"cef": _format_cef(ci),
"bpc": str(ci.bpc),
"status": str(pg_status.error),
}
def format_pattern_generator_status_log(status: dict[str, Any]) -> list[str]:
return [
f"Total bitrate: {status['total_bitrate']}",
f" Timing: {status['timing']}",
f" Pattern: {status['pattern']}",
f" CEF: {status['cef']}",
f" BPC: {status['bpc']}",
f" Status: {status['status']}",
]
def read_pattern_generator_status_lines(
pg,
*,
pattern_hint: str | None = None,
) -> list[str]:
status = collect_pattern_generator_status(pg, pattern_hint=pattern_hint)
return format_pattern_generator_status_log(status)

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import logging
import os
from app.ucd.domain import (
Colorimetry,
@@ -33,6 +34,21 @@ class SignalService:
def __init__(self, device: IUcdDevice, bus: EventBus):
self._dev = device
self._bus = bus
self._pg_status_log_callback = None
def set_pg_status_log_callback(self, callback) -> None:
"""设置 PG 状态日志回调:``callback(lines: list[str]) -> None``。"""
self._pg_status_log_callback = callback
def _notify_pattern_sent(self, pattern_hint: str | None = None) -> None:
if not self._pg_status_log_callback:
return
try:
lines = self.get_pattern_generator_status_lines(pattern_hint)
if lines:
self._pg_status_log_callback(lines)
except Exception:
log.exception("SignalService._notify_pattern_sent failed")
# -- 高层接口 ------------------------------------------------
@@ -59,17 +75,30 @@ class SignalService:
self._dev.apply()
return changed
def send_pattern(self, pattern: PatternSpec) -> None:
def send_pattern(
self,
pattern: PatternSpec,
*,
pattern_hint: str | None = None,
) -> None:
"""在已 configure 的信号上仅更新图案后 apply。"""
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern)
self._dev.apply()
self._notify_pattern_sent(pattern_hint)
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
self.send_pattern(solid_rgb_pattern(rgb))
rgb_list = list(rgb)
self.send_pattern(
solid_rgb_pattern(rgb_list),
pattern_hint=f"Solid Color {rgb_list}",
)
def send_image(self, path: str) -> None:
self.send_pattern(image_pattern(path))
self.send_pattern(
image_pattern(path),
pattern_hint=os.path.basename(path) or "Image File",
)
def update_signal_format(
self,
@@ -127,9 +156,26 @@ class SignalService:
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern不 apply 输出)。"""
return bool(self._dev.set_ucd_params(config))
def send_pattern_params(self, params) -> bool:
def send_pattern_params(
self,
params,
*,
pattern_hint: str | None = None,
) -> bool:
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
return bool(self._dev.send_current_pattern_params(params))
ok = bool(self._dev.send_current_pattern_params(params))
if ok:
self._notify_pattern_sent(pattern_hint)
return ok
def get_pattern_generator_status_lines(
self,
pattern_hint: str | None = None,
) -> list[str]:
getter = getattr(self._dev, "get_pattern_generator_status_lines", None)
if not callable(getter):
return []
return list(getter(pattern_hint) or [])
def apply_and_run(self, config, pattern_params) -> bool:
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
@@ -380,7 +426,13 @@ class PatternService:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.signal_service.send_pattern_params(pattern_param):
pattern_hint = None
if session.display_names and index < len(session.display_names):
pattern_hint = session.display_names[index]
if not self.app.signal_service.send_pattern_params(
pattern_param,
pattern_hint=pattern_hint,
):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param

View File

@@ -0,0 +1,2 @@
"""兼容层:请改用 ``app.solid_color_scale``。"""
from app.solid_color_scale import * # noqa: F403

View File

@@ -22,6 +22,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from app.tests.color_accuracy import calculate_delta_e_2000
from app.solid_color_scale import (
SOLID_COLOR_MAX,
pct_to_solid_color,
solid_color_to_byte,
solid_color_to_pct,
)
from app.views.modern_styles import get_theme_palette
if TYPE_CHECKING:
@@ -41,10 +47,15 @@ DE_FORMULAS = ["2000", "94", "76"]
def _pct_to_gray_rgb(pct: int) -> tuple[int, int, int]:
value = int(round(pct * 255 / 100))
value = solid_color_to_byte(pct_to_solid_color(pct))
return value, value, value
def _pct_to_pattern_rgb(pct: int) -> int:
"""灰阶百分比 → SolidColor 码值065535"""
return pct_to_solid_color(pct)
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
r, g, b = rgb
return f"#{r:02x}{g:02x}{b:02x}"
@@ -325,6 +336,8 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self.calman_stop_event = threading.Event()
self.calman_running = False
self.calman_patch_send_busy = False
self.calman_rgb_session = None
self.calman_rgb_session_test_type = None
self.calman_current_level = None
self.calman_last_record = None
self.calman_last_step_seconds = None
@@ -728,6 +741,29 @@ def toggle_calman_panel(self: "PQAutomationApp") -> None:
# ---------------------------------------------------------------------------
def _get_calman_rgb_session(self: "PQAutomationApp", test_type: str):
"""复用 Calman 发图 session避免每次点击都重新 stage profile。"""
cached = getattr(self, "calman_rgb_session", None)
if cached is not None and getattr(self, "calman_rgb_session_test_type", None) == test_type:
return cached
if not hasattr(self, "pattern_service") or self.pattern_service is None:
return None
session = self.pattern_service.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
self.calman_rgb_session = session
self.calman_rgb_session_test_type = test_type
_calman_log(self, f"calman rgb session prepared test_type={test_type}")
return session
def _clear_calman_rgb_session(self: "PQAutomationApp") -> None:
self.calman_rgb_session = None
self.calman_rgb_session_test_type = None
def send_patch(self: "PQAutomationApp", pct: int) -> None:
"""点击色块时,发送对应灰阶图案到信号发生器。"""
if not self.signal_service.is_connected:
@@ -738,7 +774,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
self.calman_status_var.set("发送进行中,请稍候...")
return
rgb_val = int(round(pct * 255 / 100))
rgb_val = _pct_to_pattern_rgb(pct)
self.calman_current_level = pct
self.calman_status_var.set(f"发送 {pct}%RGB={rgb_val}...")
_highlight_patch(self, pct)
@@ -751,8 +787,10 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
_calman_log(self, f"send_solid_rgb start pct={pct}")
test_type = getattr(self.config, "current_test_type", "screen_module")
if hasattr(self, "pattern_service") and self.pattern_service is not None:
rgb_session = _get_calman_rgb_session(self, test_type)
self.pattern_service.send_rgb(
(rgb_val, rgb_val, rgb_val),
session=rgb_session,
test_type=test_type,
)
else:
@@ -902,11 +940,7 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
test_type = getattr(self.config, "current_test_type", "screen_module")
rgb_session = None
if hasattr(self, "pattern_service") and self.pattern_service is not None:
rgb_session = self.pattern_service.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
rgb_session = _get_calman_rgb_session(self, test_type)
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
order = sorted(self.calman_levels, reverse=True)
@@ -918,7 +952,7 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
break
step_t0 = time.perf_counter()
rgb_val = int(round(pct * 255 / 100))
rgb_val = _pct_to_pattern_rgb(pct)
self._dispatch_ui(
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
)

View File

@@ -24,6 +24,12 @@ from typing import TYPE_CHECKING
import ttkbootstrap as ttk
from app.pq import pq_config
from app.solid_color_scale import (
SOLID_COLOR_MAX,
pct_to_solid_color,
solid_color_to_byte,
solid_color_to_pct,
)
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -56,19 +62,21 @@ def _gray_pct_of(rgb) -> str:
r = int(rgb[0])
except Exception:
return ""
return str(int(round(r / 255 * 100)))
return str(int(round(solid_color_to_pct(r))))
def _hex_of(rgb) -> str:
try:
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
r = solid_color_to_byte(rgb[0])
g = solid_color_to_byte(rgb[1])
b = solid_color_to_byte(rgb[2])
except Exception:
return ""
return f"#{r:02X}{g:02X}{b:02X}"
def _luminance(rgb) -> float:
r, g, b = rgb
r, g, b = solid_color_to_byte(rgb[0]), solid_color_to_byte(rgb[1]), solid_color_to_byte(rgb[2])
return 0.2126 * r + 0.7152 * g + 0.0722 * b
@@ -99,7 +107,7 @@ def _gen_even(n: int) -> list[list[int]]:
out = []
for i in range(n):
pct = 100.0 - (100.0 / (n - 1)) * i
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
out.append([v, v, v])
return out
@@ -118,7 +126,7 @@ def _gen_pq(n: int) -> list[list[int]]:
else:
Lm1 = L ** m1
v_pq = ((c1 + c2 * Lm1) / (1.0 + c3 * Lm1)) ** m2
v = int(round(max(0.0, min(1.0, v_pq)) * 255))
v = int(round(max(0.0, min(1.0, v_pq)) * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -129,7 +137,7 @@ def _gen_gamma(n: int, gamma: float = 2.2) -> list[list[int]]:
out = []
for i in range(n):
lin = 1.0 - i / (n - 1)
v = int(round((lin ** (1.0 / gamma)) * 255))
v = int(round((lin ** (1.0 / gamma)) * SOLID_COLOR_MAX))
out.append([v, v, v])
return out
@@ -761,8 +769,8 @@ def _parse_rgb_from_form(self: "PQAutomationApp"):
except ValueError:
messagebox.showerror("输入错误", "R/G/B 必须为整数")
return None
if not all(0 <= v <= 255 for v in (r, g, b)):
messagebox.showerror("输入错误", "R/G/B 必须在 0~255 范围内")
if not all(0 <= v <= SOLID_COLOR_MAX for v in (r, g, b)):
messagebox.showerror("输入错误", f"R/G/B 必须在 0~{SOLID_COLOR_MAX} 范围内")
return None
return [r, g, b]
@@ -775,7 +783,7 @@ def _fill_rgb_from_pct(self: "PQAutomationApp"):
messagebox.showerror("输入错误", "灰度 % 必须为数字")
return
pct = max(0.0, min(100.0, pct))
v = int(round(pct / 100.0 * 255))
v = pct_to_solid_color(pct)
self._gamma_edit_r_var.set(str(v))
self._gamma_edit_g_var.set(str(v))
self._gamma_edit_b_var.set(str(v))
@@ -904,7 +912,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
if s.endswith("%"):
try:
pct = float(s.rstrip("%"))
v = int(round(max(0, min(100, pct)) / 100.0 * 255))
v = pct_to_solid_color(max(0, min(100, pct)))
rows.append([v, v, v])
continue
except ValueError:
@@ -914,7 +922,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
if len(parts) == 1:
try:
v = int(parts[0])
if 0 <= v <= 255:
if 0 <= v <= SOLID_COLOR_MAX:
rows.append([v, v, v])
continue
except ValueError:
@@ -929,7 +937,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
except ValueError:
errors.append(f"{ln_no} 行非整数:{s}")
continue
if not all(0 <= v <= 255 for v in (r, g, b)):
if not all(0 <= v <= SOLID_COLOR_MAX for v in (r, g, b)):
errors.append(f"{ln_no} 行越界:{s}")
continue
rows.append([r, g, b])
@@ -980,7 +988,7 @@ def _run_validation(self: "PQAutomationApp"):
bad = [
i for i, rgb in enumerate(params)
if not all(0 <= int(v) <= 255 for v in rgb)
if not all(0 <= int(v) <= SOLID_COLOR_MAX for v in rgb)
]
if bad:
msgs.append(f"⚠ 越界点 (索引): {bad}")

View File

@@ -9,9 +9,14 @@ import threading
import tkinter as tk
from tkinter import filedialog, messagebox
from typing import TYPE_CHECKING
import ttkbootstrap as ttk
from typing import TYPE_CHECKING
from app.solid_color_scale import (
SOLID_COLOR_MAX,
byte_to_solid_color,
)
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
@@ -196,8 +201,12 @@ def _load_patterns(self: "PQAutomationApp"):
continue
if r is None or g is None or b is None:
continue
if min(r, g, b) < 0 or max(r, g, b) > 255:
if min(r, g, b) < 0 or max(r, g, b) > SOLID_COLOR_MAX:
continue
if max(r, g, b) <= 255:
r = byte_to_solid_color(r)
g = byte_to_solid_color(g)
b = byte_to_solid_color(b)
patterns.append((r, g, b))
finally:
wb.close()