根据新版本调整set_pattern:0~65535
This commit is contained in:
@@ -243,6 +243,13 @@ class BaseIO:
|
|||||||
self.__ref_count = tsi.TSI_Clean()
|
self.__ref_count = tsi.TSI_Clean()
|
||||||
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
|
logging.info(f"[UniTAP] BaseIO.del: {self}; Ref count: {self.__ref_count}")
|
||||||
|
|
||||||
|
def rescan_devices(self):
|
||||||
|
if self.__ref_count > 0:
|
||||||
|
tsi.TSIX_DEV_RescanDevices()
|
||||||
|
return
|
||||||
|
logging.error("[UniTAP] BaseIO.rescan_devices call without TSI_Init")
|
||||||
|
|
||||||
|
|
||||||
def get_device_count(self) -> int:
|
def get_device_count(self) -> int:
|
||||||
if self.__ref_count > 0:
|
if self.__ref_count > 0:
|
||||||
return tsi.TSIX_DEV_GetDeviceCount()
|
return tsi.TSIX_DEV_GetDeviceCount()
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ class TsiLib:
|
|||||||
self.__io.cleanup()
|
self.__io.cleanup()
|
||||||
logging.info(f"[UniTAP] TsiLib.cleanup {self}")
|
logging.info(f"[UniTAP] TsiLib.cleanup {self}")
|
||||||
|
|
||||||
|
def rescan_devices(self):
|
||||||
|
"""Rescan USB for newly connected TSI devices without tearing down the library."""
|
||||||
|
self.__io.rescan_devices()
|
||||||
|
logging.info(f"[UniTAP] TsiLib.rescan_devices {self}")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
Automatically call the destructor after the script completes.
|
Automatically call the destructor after the script completes.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import math
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
|
|
||||||
|
from app.solid_color_scale import SOLID_COLOR_MAX
|
||||||
|
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# 色域覆盖率计算公式
|
# 色域覆盖率计算公式
|
||||||
@@ -335,7 +337,7 @@ def calculate_gamma(results, max_index_fix, pattern_params=None):
|
|||||||
results: [[x, y, lv, X, Y, Z], ...] 测量结果
|
results: [[x, y, lv, X, Y, Z], ...] 测量结果
|
||||||
max_index_fix: 最大灰阶索引
|
max_index_fix: 最大灰阶索引
|
||||||
pattern_params: [[R, G, B], ...] 8bit pattern参数,用于计算input_level
|
pattern_params: [[R, G, B], ...] 8bit pattern参数,用于计算input_level
|
||||||
如果提供,则使用 pattern_value/255 作为底数
|
如果提供,则使用 pattern_value/SOLID_COLOR_MAX 作为底数
|
||||||
如果不提供,则使用传统的 灰阶百分比 作为底数
|
如果不提供,则使用传统的 灰阶百分比 作为底数
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -372,10 +374,10 @@ def calculate_gamma(results, max_index_fix, pattern_params=None):
|
|||||||
|
|
||||||
# 计算输入灰阶值 input_level
|
# 计算输入灰阶值 input_level
|
||||||
if pattern_params is not None and i < len(pattern_params):
|
if pattern_params is not None and i < len(pattern_params):
|
||||||
# 使用 8bit pattern 数据 / 255 作为底数(22293 Gamma 数据对齐)
|
# 使用 SolidColor pattern 数据 / SOLID_COLOR_MAX 作为底数(22293 Gamma 数据对齐)
|
||||||
# 取 R 通道值(灰阶图案 R=G=B)
|
# 取 R 通道值(灰阶图案 R=G=B)
|
||||||
pattern_value = pattern_params[i][0]
|
pattern_value = pattern_params[i][0]
|
||||||
input_level = pattern_value / 255.0
|
input_level = pattern_value / SOLID_COLOR_MAX
|
||||||
else:
|
else:
|
||||||
# 传统计算方式:根据数据顺序计算输入灰阶值
|
# 传统计算方式:根据数据顺序计算输入灰阶值
|
||||||
if is_descending:
|
if is_descending:
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
"""
|
"""
|
||||||
数据范围转换器
|
数据范围转换器
|
||||||
将 Full Range (0-255) 转换为 Limited Range (16-235)
|
将 Full Range (0–65535) 转换为 Limited Range(SolidColor 刻度)
|
||||||
|
|
||||||
使用方法:
|
|
||||||
from app.data_range_converter import DataRangeConverter
|
|
||||||
|
|
||||||
converter = DataRangeConverter()
|
|
||||||
converted_params = converter.convert(pattern_params, "Limited")
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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:
|
class DataRangeConverter:
|
||||||
"""数据范围转换器"""
|
"""数据范围转换器"""
|
||||||
@@ -25,38 +27,22 @@ class DataRangeConverter:
|
|||||||
|
|
||||||
def convert_value(self, value):
|
def convert_value(self, value):
|
||||||
"""
|
"""
|
||||||
将单个 Full Range 值转换为 Limited Range
|
将单个 Full Range 值转换为 Limited Range(SolidColor 刻度)。
|
||||||
|
|
||||||
转换公式:
|
转换公式(归一化后线性映射):
|
||||||
limited = 16 + (value / 255) × (235 - 16)
|
limited = L_black + (value / MAX) × (L_white - L_black)
|
||||||
|
|
||||||
Args:
|
|
||||||
value: Full Range 值 (0-255)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Limited Range 值 (16-235)
|
|
||||||
"""
|
"""
|
||||||
# 边界值直接映射
|
value = clamp_solid_color(value)
|
||||||
if value == 0:
|
if value <= SOLID_COLOR_MIN:
|
||||||
return 16
|
return LIMITED_SOLID_COLOR_BLACK
|
||||||
elif value == 255:
|
if value >= SOLID_COLOR_MAX:
|
||||||
return 235
|
return LIMITED_SOLID_COLOR_WHITE
|
||||||
else:
|
span = LIMITED_SOLID_COLOR_WHITE - LIMITED_SOLID_COLOR_BLACK
|
||||||
# 线性映射
|
limited = LIMITED_SOLID_COLOR_BLACK + round((value / SOLID_COLOR_MAX) * span)
|
||||||
limited = 16 + round((value / 255.0) * (235 - 16))
|
return max(LIMITED_SOLID_COLOR_BLACK, min(LIMITED_SOLID_COLOR_WHITE, limited))
|
||||||
# 限制范围
|
|
||||||
return max(16, min(235, limited))
|
|
||||||
|
|
||||||
def convert_rgb(self, r, g, b):
|
def convert_rgb(self, r, g, b):
|
||||||
"""
|
"""转换单个 RGB 值(SolidColor Full → Limited)。"""
|
||||||
转换单个 RGB 值
|
|
||||||
|
|
||||||
Args:
|
|
||||||
r, g, b: Full Range RGB 值 (0-255)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple: Limited Range RGB 值 (16-235)
|
|
||||||
"""
|
|
||||||
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
|
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
|
||||||
|
|
||||||
def convert(self, pattern_params, data_range="Full"):
|
def convert(self, pattern_params, data_range="Full"):
|
||||||
@@ -73,7 +59,7 @@ class DataRangeConverter:
|
|||||||
# Full Range 不需要转换
|
# Full Range 不需要转换
|
||||||
if data_range == "Full":
|
if data_range == "Full":
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print("使用 Full Range (0-255),无需转换")
|
print(f"使用 Full Range (0–{SOLID_COLOR_MAX}),无需转换")
|
||||||
return pattern_params
|
return pattern_params
|
||||||
|
|
||||||
# Limited Range 需要转换
|
# Limited Range 需要转换
|
||||||
@@ -112,8 +98,11 @@ class DataRangeConverter:
|
|||||||
def _print_header(self):
|
def _print_header(self):
|
||||||
"""打印转换头部信息"""
|
"""打印转换头部信息"""
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
print("【数据范围转换】Limited Range (16-235)")
|
print("【数据范围转换】Limited Range (SolidColor 刻度)")
|
||||||
print(" 转换公式: 16 + (value / 255) × (235 - 16)")
|
print(
|
||||||
|
f" 映射: {SOLID_COLOR_MIN}–{SOLID_COLOR_MAX} → "
|
||||||
|
f"{LIMITED_SOLID_COLOR_BLACK}–{LIMITED_SOLID_COLOR_WHITE}"
|
||||||
|
)
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
def _print_conversion(self, index, r_orig, g_orig, b_orig, r_new, g_new, b_new):
|
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
|
should_print = False
|
||||||
|
label = ""
|
||||||
|
mid = SOLID_COLOR_MAX // 2
|
||||||
|
|
||||||
# 第一个和最后一个
|
|
||||||
if index == 0:
|
if index == 0:
|
||||||
should_print = True
|
should_print = True
|
||||||
label = "黑色"
|
label = "黑色"
|
||||||
elif index == len([]) - 1: # 需要在外部判断
|
elif index == len([]) - 1: # 需要在外部判断
|
||||||
should_print = True
|
should_print = True
|
||||||
label = "白色"
|
label = "白色"
|
||||||
# 关键 RGB 值
|
|
||||||
elif (
|
elif (
|
||||||
r_orig in [0, 128, 255]
|
r_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
|
||||||
or g_orig in [0, 128, 255]
|
or g_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
|
||||||
or b_orig in [0, 128, 255]
|
or b_orig in [SOLID_COLOR_MIN, mid, SOLID_COLOR_MAX]
|
||||||
):
|
):
|
||||||
should_print = True
|
should_print = True
|
||||||
if r_orig == 128:
|
if r_orig == mid:
|
||||||
label = "50% 灰"
|
label = "50% 灰"
|
||||||
else:
|
|
||||||
label = ""
|
|
||||||
|
|
||||||
if should_print:
|
if should_print:
|
||||||
diff = abs(r_new - r_orig)
|
diff = abs(r_new - r_orig)
|
||||||
@@ -165,9 +152,9 @@ class DataRangeConverter:
|
|||||||
return {
|
return {
|
||||||
"name": "Data Range Converter",
|
"name": "Data Range Converter",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"full_range": "0-255",
|
"full_range": f"0-{SOLID_COLOR_MAX}",
|
||||||
"limited_range": "16-235",
|
"limited_range": f"{LIMITED_SOLID_COLOR_BLACK}-{LIMITED_SOLID_COLOR_WHITE}",
|
||||||
"formula": "16 + (value / 255) × 219",
|
"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
|
>>> 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")
|
>>> converted = convert_pattern_params(params, "Limited")
|
||||||
[[16,16,16], [235,235,235]]
|
[[4112,4112,4112], [60492,60492,60492]]
|
||||||
"""
|
"""
|
||||||
converter = DataRangeConverter(verbose=verbose)
|
converter = DataRangeConverter(verbose=verbose)
|
||||||
return converter.convert(pattern_params, data_range)
|
return converter.convert(pattern_params, data_range)
|
||||||
@@ -201,7 +188,7 @@ def convert_single_rgb(r, g, b, data_range="Full"):
|
|||||||
便捷函数:转换单个 RGB 值
|
便捷函数:转换单个 RGB 值
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
r, g, b: RGB 值 (0-255)
|
r, g, b: RGB 值 (0–65535)
|
||||||
data_range: "Full" 或 "Limited"
|
data_range: "Full" 或 "Limited"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -232,7 +219,8 @@ if __name__ == "__main__":
|
|||||||
print("\n[测试 1] 基本转换...")
|
print("\n[测试 1] 基本转换...")
|
||||||
converter = DataRangeConverter(verbose=False)
|
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:")
|
print(" Full Range → Limited Range:")
|
||||||
for v in test_values:
|
for v in test_values:
|
||||||
limited = converter.convert_value(v)
|
limited = converter.convert_value(v)
|
||||||
@@ -243,8 +231,8 @@ if __name__ == "__main__":
|
|||||||
print("\n[测试 2] RGB 转换...")
|
print("\n[测试 2] RGB 转换...")
|
||||||
test_rgb = [
|
test_rgb = [
|
||||||
(0, 0, 0),
|
(0, 0, 0),
|
||||||
(128, 128, 128),
|
(SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2, SOLID_COLOR_MAX // 2),
|
||||||
(255, 255, 255),
|
(SOLID_COLOR_MAX, SOLID_COLOR_MAX, SOLID_COLOR_MAX),
|
||||||
]
|
]
|
||||||
|
|
||||||
for r, g, b in test_rgb:
|
for r, g, b in test_rgb:
|
||||||
@@ -253,12 +241,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 测试 3: 完整转换流程
|
# 测试 3: 完整转换流程
|
||||||
print("\n[测试 3] 完整转换流程...")
|
print("\n[测试 3] 完整转换流程...")
|
||||||
|
from app.ucd.solid_color_scale import byte_to_solid_color as _sc
|
||||||
|
|
||||||
pattern_params = [
|
pattern_params = [
|
||||||
[255, 255, 255], # 100% 白
|
[_sc(255)] * 3,
|
||||||
[230, 230, 230], # 90%
|
[_sc(230)] * 3,
|
||||||
[204, 204, 204], # 80%
|
[_sc(204)] * 3,
|
||||||
[128, 128, 128], # 50%
|
[_sc(128)] * 3,
|
||||||
[0, 0, 0], # 0% 黑
|
[0, 0, 0],
|
||||||
]
|
]
|
||||||
|
|
||||||
converted = converter.convert(pattern_params, "Limited")
|
converted = converter.convert(pattern_params, "Limited")
|
||||||
@@ -270,11 +260,11 @@ if __name__ == "__main__":
|
|||||||
# 测试 4: 便捷函数
|
# 测试 4: 便捷函数
|
||||||
print("\n[测试 4] 便捷函数...")
|
print("\n[测试 4] 便捷函数...")
|
||||||
result = convert_pattern_params(
|
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}")
|
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})")
|
print(f" RGB(128,128,128) → RGB({r},{g},{b})")
|
||||||
|
|
||||||
# 测试 5: 获取信息
|
# 测试 5: 获取信息
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from app.solid_color_scale import solid_color_to_pct
|
||||||
from app.views.modern_styles import get_theme_palette
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
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曲线 + 数据表格(包含实测亮度)"""
|
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
||||||
palette = get_theme_palette()
|
palette = get_theme_palette()
|
||||||
dark_mode = _is_dark_palette(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():
|
for spine in self.gamma_ax.spines.values():
|
||||||
spine.set_color(palette["border"])
|
spine.set_color(palette["border"])
|
||||||
|
|
||||||
# 生成横坐标(灰阶百分比)
|
# 横坐标:优先用 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))
|
x_values = np.linspace(0, 100, len(L_bar))
|
||||||
|
|
||||||
# 反转 L_bar(确保从左到右是 0% → 100%)
|
# 反转 L_bar(确保从左到右是 0% → 100%)
|
||||||
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
|
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
|
||||||
L_bar = L_bar[::-1]
|
L_bar = L_bar[::-1]
|
||||||
results_with_gamma_list = results_with_gamma_list[::-1]
|
results_with_gamma_list = results_with_gamma_list[::-1]
|
||||||
|
x_values = x_values[::-1]
|
||||||
|
|
||||||
# 计算平均Gamma
|
# 计算平均Gamma
|
||||||
gamma_values = []
|
gamma_values = []
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import json
|
|||||||
import copy
|
import copy
|
||||||
from pathlib import Path
|
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)
|
# Pattern 文件读写工具(统一存储格式:settings/patterns/{name}.json)
|
||||||
@@ -25,7 +32,7 @@ def load_pattern_file(filepath) -> dict:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
with open(filepath, encoding="utf-8") as f:
|
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:
|
def save_pattern_file(filepath, pattern: dict) -> None:
|
||||||
@@ -137,9 +144,9 @@ _PATTERN_RGB = {
|
|||||||
"measurement_bit_depth": 8,
|
"measurement_bit_depth": 8,
|
||||||
"measurement_max_value": 2,
|
"measurement_max_value": 2,
|
||||||
"pattern_params": [
|
"pattern_params": [
|
||||||
[255, 0, 0], # 红色
|
_rgb(255, 0, 0), # 红色
|
||||||
[0, 255, 0], # 绿色
|
_rgb(0, 255, 0), # 绿色
|
||||||
[0, 0, 255], # 蓝色
|
_rgb(0, 0, 255), # 蓝色
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,17 +156,17 @@ _PATTERN_GRAY_FALLBACK = {
|
|||||||
"measurement_bit_depth": 8,
|
"measurement_bit_depth": 8,
|
||||||
"measurement_max_value": 10,
|
"measurement_max_value": 10,
|
||||||
"pattern_params": [
|
"pattern_params": [
|
||||||
[255, 255, 255], # 100% 白色
|
_rgb(255, 255, 255), # 100% 白色
|
||||||
[230, 230, 230], # 90%
|
_rgb(230, 230, 230), # 90%
|
||||||
[205, 205, 205], # 80%
|
_rgb(205, 205, 205), # 80%
|
||||||
[179, 179, 179], # 70%
|
_rgb(179, 179, 179), # 70%
|
||||||
[154, 154, 154], # 60%
|
_rgb(154, 154, 154), # 60%
|
||||||
[128, 128, 128], # 50%
|
_rgb(128, 128, 128), # 50%
|
||||||
[102, 102, 102], # 40%
|
_rgb(102, 102, 102), # 40%
|
||||||
[78, 78, 78], # 30%
|
_rgb(78, 78, 78), # 30%
|
||||||
[52, 52, 52], # 20%
|
_rgb(52, 52, 52), # 20%
|
||||||
[26, 26, 26], # 10%
|
_rgb(26, 26, 26), # 10%
|
||||||
[0, 0, 0], # 0% 黑色
|
_rgb(0, 0, 0), # 0% 黑色
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# 灰阶 pattern 从文件加载,支持用户编辑;文件缺失时回退到硬编码兜底
|
# 灰阶 pattern 从文件加载,支持用户编辑;文件缺失时回退到硬编码兜底
|
||||||
@@ -171,37 +178,37 @@ _PATTERN_ACCURACY = {
|
|||||||
"measurement_max_value": 28, # 29个颜色,最大索引是28
|
"measurement_max_value": 28, # 29个颜色,最大索引是28
|
||||||
"pattern_params": [
|
"pattern_params": [
|
||||||
# ========== 灰阶 (5个) ==========
|
# ========== 灰阶 (5个) ==========
|
||||||
[255, 255, 255], # 0: White
|
_rgb(255, 255, 255), # 0: White
|
||||||
[230, 230, 230], # 1: Gray 80
|
_rgb(230, 230, 230), # 1: Gray 80
|
||||||
[209, 209, 209], # 2: Gray 65
|
_rgb(209, 209, 209), # 2: Gray 65
|
||||||
[186, 186, 186], # 3: Gray 50
|
_rgb(186, 186, 186), # 3: Gray 50
|
||||||
[158, 158, 158], # 4: Gray 35
|
_rgb(158, 158, 158), # 4: Gray 35
|
||||||
# ========== ColorChecker 24色 (18个) ==========
|
# ========== ColorChecker 24色 (18个) ==========
|
||||||
[115, 82, 66], # 5: Dark Skin
|
_rgb(115, 82, 66), # 5: Dark Skin
|
||||||
[194, 150, 130], # 6: Light Skin
|
_rgb(194, 150, 130), # 6: Light Skin
|
||||||
[94, 122, 156], # 7: Blue Sky
|
_rgb(94, 122, 156), # 7: Blue Sky
|
||||||
[89, 107, 66], # 8: Foliage
|
_rgb(89, 107, 66), # 8: Foliage
|
||||||
[130, 128, 176], # 9: Blue Flower
|
_rgb(130, 128, 176), # 9: Blue Flower
|
||||||
[99, 189, 168], # 10: Bluish Green
|
_rgb(99, 189, 168), # 10: Bluish Green
|
||||||
[217, 120, 41], # 11: Orange
|
_rgb(217, 120, 41), # 11: Orange
|
||||||
[74, 92, 163], # 12: Purplish Blue
|
_rgb(74, 92, 163), # 12: Purplish Blue
|
||||||
[194, 84, 97], # 13: Moderate Red
|
_rgb(194, 84, 97), # 13: Moderate Red
|
||||||
[92, 61, 107], # 14: Purple
|
_rgb(92, 61, 107), # 14: Purple
|
||||||
[158, 186, 64], # 15: Yellow Green
|
_rgb(158, 186, 64), # 15: Yellow Green
|
||||||
[230, 161, 46], # 16: Orange Yellow
|
_rgb(230, 161, 46), # 16: Orange Yellow
|
||||||
[51, 61, 150], # 17: Blue (Legacy)
|
_rgb(51, 61, 150), # 17: Blue (Legacy)
|
||||||
[71, 148, 71], # 18: Green (Legacy)
|
_rgb(71, 148, 71), # 18: Green (Legacy)
|
||||||
[176, 48, 59], # 19: Red (Legacy)
|
_rgb(176, 48, 59), # 19: Red (Legacy)
|
||||||
[237, 199, 33], # 20: Yellow (Legacy)
|
_rgb(237, 199, 33), # 20: Yellow (Legacy)
|
||||||
[186, 84, 145], # 21: Magenta (Legacy)
|
_rgb(186, 84, 145), # 21: Magenta (Legacy)
|
||||||
[0, 133, 163], # 22: Cyan (Legacy)
|
_rgb(0, 133, 163), # 22: Cyan (Legacy)
|
||||||
# ========== 100% 饱和色 (6个) ==========
|
# ========== 100% 饱和色 (6个) ==========
|
||||||
[255, 0, 0], # 23: 100% Red
|
_rgb(255, 0, 0), # 23: 100% Red
|
||||||
[0, 255, 0], # 24: 100% Green
|
_rgb(0, 255, 0), # 24: 100% Green
|
||||||
[0, 0, 255], # 25: 100% Blue
|
_rgb(0, 0, 255), # 25: 100% Blue
|
||||||
[0, 255, 255], # 26: 100% Cyan
|
_rgb(0, 255, 255), # 26: 100% Cyan
|
||||||
[255, 0, 255], # 27: 100% Magenta
|
_rgb(255, 0, 255), # 27: 100% Magenta
|
||||||
[255, 255, 0], # 28: 100% Yellow
|
_rgb(255, 255, 0), # 28: 100% Yellow
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +512,7 @@ def _gen_even_gray(n: int) -> list[list[int]]:
|
|||||||
out = []
|
out = []
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
pct = 100.0 - (100.0 / (n - 1)) * i
|
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])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -521,7 +528,7 @@ def _gen_pq_gray(n: int) -> list[list[int]]:
|
|||||||
out = []
|
out = []
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
v_pq = 1.0 - i / (n - 1)
|
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])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -534,7 +541,7 @@ def _gen_gamma_gray(n: int, gamma: float = 2.2) -> list[list[int]]:
|
|||||||
for i in range(n):
|
for i in range(n):
|
||||||
lin = 1.0 - i / (n - 1) # 线性光 1→0
|
lin = 1.0 - i / (n - 1) # 线性光 1→0
|
||||||
code = lin ** (1.0 / gamma) # gamma 编码
|
code = lin ** (1.0 / gamma) # gamma 编码
|
||||||
v = int(round(code * 255))
|
v = int(round(code * SOLID_COLOR_MAX))
|
||||||
out.append([v, v, v])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|||||||
@@ -51,12 +51,13 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
|
|||||||
, level="info")
|
, level="info")
|
||||||
|
|
||||||
# 根据测试类型执行不同的测试流程
|
# 根据测试类型执行不同的测试流程
|
||||||
|
ok = True
|
||||||
if test_type == "screen_module":
|
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":
|
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":
|
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":
|
elif test_type == "local_dimming":
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
"Local Dimming 为手动模式,请在 Local Dimming 面板发送图案并采集亮度",
|
"Local Dimming 为手动模式,请在 Local Dimming 面板发送图案并采集亮度",
|
||||||
@@ -65,7 +66,10 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
|
|||||||
|
|
||||||
# 测试完成后更新UI状态
|
# 测试完成后更新UI状态
|
||||||
if self.testing: # 如果没有被中途停止
|
if self.testing: # 如果没有被中途停止
|
||||||
|
if ok:
|
||||||
self._dispatch_ui(self.on_test_completed)
|
self._dispatch_ui(self.on_test_completed)
|
||||||
|
else:
|
||||||
|
self._dispatch_ui(self.on_test_error)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"测试过程中发生错误: {str(e)}", level="info")
|
self.log_gui.log(f"测试过程中发生错误: {str(e)}", level="info")
|
||||||
import traceback
|
import traceback
|
||||||
@@ -82,7 +86,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
|
|||||||
self.new_pq_results("screen_module", "屏模组性能测试")
|
self.new_pq_results("screen_module", "屏模组性能测试")
|
||||||
else:
|
else:
|
||||||
self.log_gui.log("未选择任何测试项目", level="info")
|
self.log_gui.log("未选择任何测试项目", level="info")
|
||||||
return
|
return False
|
||||||
|
|
||||||
# 判断是否需要灰阶数据
|
# 判断是否需要灰阶数据
|
||||||
needs_gray_data = any(
|
needs_gray_data = any(
|
||||||
@@ -96,7 +100,7 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
|
|||||||
|
|
||||||
for item in test_items:
|
for item in test_items:
|
||||||
if not self.testing: # 检查是否被停止
|
if not self.testing: # 检查是否被停止
|
||||||
return
|
return False
|
||||||
|
|
||||||
current_item += 1
|
current_item += 1
|
||||||
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
|
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:
|
if not shared_gray_data or len(shared_gray_data) < 2:
|
||||||
self.log_gui.log("灰阶数据采集失败或数据不足,跳过相关测试", level="error")
|
self.log_gui.log("灰阶数据采集失败或数据不足,跳过相关测试", level="error")
|
||||||
return
|
return False
|
||||||
|
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"灰阶数据采集完成,共 {len(shared_gray_data)} 个数据点"
|
f"灰阶数据采集完成,共 {len(shared_gray_data)} 个数据点"
|
||||||
@@ -148,6 +152,8 @@ def run_screen_module_test(self: "PQAutomationApp", test_items):
|
|||||||
elif item == "contrast":
|
elif item == "contrast":
|
||||||
self.test_contrast("screen_module", shared_gray_data)
|
self.test_contrast("screen_module", shared_gray_data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_custom_sdr_test(self: "PQAutomationApp", test_items):
|
def run_custom_sdr_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行客户定制 SDR 测试 - 升级版"""
|
"""执行客户定制 SDR 测试 - 升级版"""
|
||||||
@@ -173,7 +179,7 @@ def run_sdr_movie_test(self: "PQAutomationApp", test_items):
|
|||||||
self.new_pq_results("sdr_movie", "SDR Movie测试")
|
self.new_pq_results("sdr_movie", "SDR Movie测试")
|
||||||
else:
|
else:
|
||||||
self.log_gui.log("未选择任何测试项目", level="info")
|
self.log_gui.log("未选择任何测试项目", level="info")
|
||||||
return
|
return False
|
||||||
|
|
||||||
# 获取信号格式设置
|
# 获取信号格式设置
|
||||||
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
|
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:
|
for item in test_items:
|
||||||
if not self.testing:
|
if not self.testing:
|
||||||
return
|
return False
|
||||||
|
|
||||||
current_item += 1
|
current_item += 1
|
||||||
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
|
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:
|
if not shared_gray_data or len(shared_gray_data) < 2:
|
||||||
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
|
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
|
||||||
return
|
return False
|
||||||
|
|
||||||
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
|
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":
|
elif item == "accuracy":
|
||||||
self.test_color_accuracy("sdr_movie")
|
self.test_color_accuracy("sdr_movie")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def run_hdr_movie_test(self: "PQAutomationApp", test_items):
|
def run_hdr_movie_test(self: "PQAutomationApp", test_items):
|
||||||
"""执行HDR Movie测试"""
|
"""执行HDR Movie测试"""
|
||||||
@@ -244,7 +252,7 @@ def run_hdr_movie_test(self: "PQAutomationApp", test_items):
|
|||||||
self.new_pq_results("hdr_movie", "HDR Movie测试")
|
self.new_pq_results("hdr_movie", "HDR Movie测试")
|
||||||
else:
|
else:
|
||||||
self.log_gui.log("未选择任何测试项目", level="info")
|
self.log_gui.log("未选择任何测试项目", level="info")
|
||||||
return
|
return False
|
||||||
|
|
||||||
# 获取信号格式设置
|
# 获取信号格式设置
|
||||||
color_space = self.hdr_color_space_var.get()
|
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:
|
for item in test_items:
|
||||||
if not self.testing:
|
if not self.testing:
|
||||||
return
|
return False
|
||||||
|
|
||||||
current_item += 1
|
current_item += 1
|
||||||
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
|
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:
|
if not shared_gray_data or len(shared_gray_data) < 2:
|
||||||
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
|
self.log_gui.log("灰阶数据采集失败或数据不足", level="error")
|
||||||
return
|
return False
|
||||||
|
|
||||||
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
|
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":
|
elif item == "accuracy":
|
||||||
self.test_color_accuracy("hdr_movie")
|
self.test_color_accuracy("hdr_movie")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def send_fix_pattern(self: "PQAutomationApp", mode):
|
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 参数(用于22293 Gamma数据对齐)
|
||||||
pattern_params = self.config.default_pattern_gray.get(
|
pattern_params = self.config.current_pattern.get(
|
||||||
"pattern_params", None
|
"pattern_params", None
|
||||||
)
|
) or self.config.default_pattern_gray.get("pattern_params", None)
|
||||||
|
|
||||||
# 计算Gamma值(使用修正后的 max_index_fix 和 8bit pattern参数)
|
# 计算Gamma值(使用修正后的 max_index_fix 和 8bit pattern参数)
|
||||||
results_with_gamma_list, L_bar = self.calculate_gamma(
|
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
|
target_gamma = 2.2
|
||||||
else:
|
else:
|
||||||
target_gamma = 2.2
|
target_gamma = 2.2
|
||||||
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
|
self.plot_gamma(
|
||||||
self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type))
|
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")
|
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")
|
self.log_gui.log(f"最终使用的 max_index_fix = {max_index_fix}", level="info")
|
||||||
|
|
||||||
# 获取灰阶 pattern 参数(用于22293 Gamma数据对齐)
|
# 获取灰阶 pattern 参数(用于22293 Gamma数据对齐)
|
||||||
pattern_params = self.config.default_pattern_gray.get(
|
pattern_params = self.config.current_pattern.get(
|
||||||
"pattern_params", None
|
"pattern_params", None
|
||||||
)
|
) or self.config.default_pattern_gray.get("pattern_params", None)
|
||||||
|
|
||||||
# ========== 计算 EOTF(复用 Gamma 计算逻辑,使用8bit pattern参数)==========
|
# ========== 计算 EOTF(复用 Gamma 计算逻辑,使用8bit pattern参数)==========
|
||||||
results_with_eotf_list, L_bar = self.calculate_gamma(
|
results_with_eotf_list, L_bar = self.calculate_gamma(
|
||||||
|
|||||||
118
app/solid_color_scale.py
Normal file
118
app/solid_color_scale.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"""UCD SolidColor 码值刻度(与 UniTAP ``SolidColorParams`` 一致,0–65535)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
SOLID_COLOR_MIN = 0
|
||||||
|
SOLID_COLOR_MAX = 65535
|
||||||
|
# 官方 work_with_solid_color.py:65535 刻度需 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:
|
||||||
|
"""灰阶/亮度百分比 (0–100) → 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 码值 → 百分比 (0–100)。"""
|
||||||
|
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 码值 → 8bit(UI 色块 / HEX 显示用)。"""
|
||||||
|
return int(round(clamp_solid_color(value) / SOLID_COLOR_MAX * 255))
|
||||||
|
|
||||||
|
|
||||||
|
def byte_to_solid_color(value: int | float) -> int:
|
||||||
|
"""旧 8bit 码值 (0–255) → 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`` 是否仍为旧 0–255 刻度。"""
|
||||||
|
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:
|
||||||
|
"""将 0–255 刻度的 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 直接接受 0–65535,须绕过 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",
|
||||||
|
]
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
import math
|
import math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from app.solid_color_scale import solid_color_rgb as _rgb
|
||||||
|
|
||||||
|
|
||||||
D65_X = 0.3127
|
D65_X = 0.3127
|
||||||
D65_Y = 0.3290
|
D65_Y = 0.3290
|
||||||
@@ -150,35 +152,35 @@ _GRAYSCALE_SIGNAL = {
|
|||||||
|
|
||||||
|
|
||||||
_SDR_COLOR_PATTERNS = [
|
_SDR_COLOR_PATTERNS = [
|
||||||
("White", 255, 255, 255),
|
("White", *_rgb(255, 255, 255)),
|
||||||
("Gray 80", 230, 230, 230),
|
("Gray 80", *_rgb(230, 230, 230)),
|
||||||
("Gray 65", 209, 209, 209),
|
("Gray 65", *_rgb(209, 209, 209)),
|
||||||
("Gray 50", 186, 186, 186),
|
("Gray 50", *_rgb(186, 186, 186)),
|
||||||
("Gray 35", 158, 158, 158),
|
("Gray 35", *_rgb(158, 158, 158)),
|
||||||
("Dark Skin", 115, 82, 66),
|
("Dark Skin", *_rgb(115, 82, 66)),
|
||||||
("Light Skin", 194, 150, 130),
|
("Light Skin", *_rgb(194, 150, 130)),
|
||||||
("Blue Sky", 94, 122, 156),
|
("Blue Sky", *_rgb(94, 122, 156)),
|
||||||
("Foliage", 89, 107, 66),
|
("Foliage", *_rgb(89, 107, 66)),
|
||||||
("Blue Flower", 130, 128, 176),
|
("Blue Flower", *_rgb(130, 128, 176)),
|
||||||
("Bluish Green", 99, 189, 168),
|
("Bluish Green", *_rgb(99, 189, 168)),
|
||||||
("Orange", 217, 120, 41),
|
("Orange", *_rgb(217, 120, 41)),
|
||||||
("Purplish Blue", 74, 92, 163),
|
("Purplish Blue", *_rgb(74, 92, 163)),
|
||||||
("Moderate Red", 194, 84, 97),
|
("Moderate Red", *_rgb(194, 84, 97)),
|
||||||
("Purple", 92, 61, 107),
|
("Purple", *_rgb(92, 61, 107)),
|
||||||
("Yellow Green", 158, 186, 64),
|
("Yellow Green", *_rgb(158, 186, 64)),
|
||||||
("Orange Yellow", 230, 161, 46),
|
("Orange Yellow", *_rgb(230, 161, 46)),
|
||||||
("Blue (Legacy)", 51, 61, 150),
|
("Blue (Legacy)", *_rgb(51, 61, 150)),
|
||||||
("Green (Legacy)", 71, 148, 71),
|
("Green (Legacy)", *_rgb(71, 148, 71)),
|
||||||
("Red (Legacy)", 176, 48, 59),
|
("Red (Legacy)", *_rgb(176, 48, 59)),
|
||||||
("Yellow (Legacy)", 237, 199, 33),
|
("Yellow (Legacy)", *_rgb(237, 199, 33)),
|
||||||
("Magenta (Legacy)", 186, 84, 145),
|
("Magenta (Legacy)", *_rgb(186, 84, 145)),
|
||||||
("Cyan (Legacy)", 0, 133, 163),
|
("Cyan (Legacy)", *_rgb(0, 133, 163)),
|
||||||
("100% Red", 255, 0, 0),
|
("100% Red", *_rgb(255, 0, 0)),
|
||||||
("100% Green", 0, 255, 0),
|
("100% Green", *_rgb(0, 255, 0)),
|
||||||
("100% Blue", 0, 0, 255),
|
("100% Blue", *_rgb(0, 0, 255)),
|
||||||
("100% Cyan", 0, 255, 255),
|
("100% Cyan", *_rgb(0, 255, 255)),
|
||||||
("100% Magenta", 255, 0, 255),
|
("100% Magenta", *_rgb(255, 0, 255)),
|
||||||
("100% Yellow", 255, 255, 0),
|
("100% Yellow", *_rgb(255, 255, 0)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ from app.ucd.device import (
|
|||||||
list_devices,
|
list_devices,
|
||||||
)
|
)
|
||||||
from app.ucd.service import PatternService, PatternSession, SignalService
|
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__ = [
|
__all__ = [
|
||||||
"SignalService",
|
"SignalService",
|
||||||
@@ -25,4 +39,15 @@ __all__ = [
|
|||||||
"EventBus",
|
"EventBus",
|
||||||
"ConnectionChanged",
|
"ConnectionChanged",
|
||||||
"DeviceKind",
|
"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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ from app.ucd.domain import (
|
|||||||
is_ycbcr,
|
is_ycbcr,
|
||||||
)
|
)
|
||||||
from app.ucd.enum import UCDEnum
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
|
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
|
||||||
@@ -108,6 +110,7 @@ class _UcdSdkBackend:
|
|||||||
self.current_pattern_params = None
|
self.current_pattern_params = None
|
||||||
self.current_pattern_index = 0
|
self.current_pattern_index = 0
|
||||||
self.last_error = None
|
self.last_error = None
|
||||||
|
self._solid_color_stream_ready = False
|
||||||
|
|
||||||
def search_device(self):
|
def search_device(self):
|
||||||
"""搜索可用设备"""
|
"""搜索可用设备"""
|
||||||
@@ -213,6 +216,10 @@ class _UcdSdkBackend:
|
|||||||
self.current_pattern_params = None
|
self.current_pattern_params = None
|
||||||
self.current_pattern_index = 0
|
self.current_pattern_index = 0
|
||||||
self.current_interface = "HDMI"
|
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):
|
def _force_cleanup(self):
|
||||||
"""强制清理所有状态"""
|
"""强制清理所有状态"""
|
||||||
@@ -440,6 +447,7 @@ class _UcdSdkBackend:
|
|||||||
pg, _ = self.get_tx_modules()
|
pg, _ = self.get_tx_modules()
|
||||||
log.info("UcdSdk.set_video_mode calling pg.set_vm()")
|
log.info("UcdSdk.set_video_mode calling pg.set_vm()")
|
||||||
pg.set_vm(vm=video_mode)
|
pg.set_vm(vm=video_mode)
|
||||||
|
self._invalidate_solid_color_stream()
|
||||||
self._stop_audio_output()
|
self._stop_audio_output()
|
||||||
log.info("UcdSdk.set_video_mode done")
|
log.info("UcdSdk.set_video_mode done")
|
||||||
self._last_sent_config = current_config
|
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))
|
log.warning("UcdSdk.set_pattern_params unsupported pattern=%s", getattr(pattern, "name", pattern))
|
||||||
return False
|
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):
|
def apply_pattern(self):
|
||||||
"""应用当前pattern"""
|
"""应用当前pattern"""
|
||||||
if self.current_pattern is not None:
|
if self.current_pattern is not None:
|
||||||
@@ -715,6 +821,7 @@ class _UcdSdkBackend:
|
|||||||
|
|
||||||
def set_ucd_params(self, config):
|
def set_ucd_params(self, config):
|
||||||
"""设置UCD323参数"""
|
"""设置UCD323参数"""
|
||||||
|
self._invalidate_solid_color_stream()
|
||||||
self.last_error = None
|
self.last_error = None
|
||||||
self.config = config
|
self.config = config
|
||||||
test_type = self.config.current_test_type
|
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")
|
log.error("UcdSdk.send_current_pattern_params failed: set_pattern returned False")
|
||||||
return 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()")
|
log.info("UcdSdk.send_current_pattern_params calling run()")
|
||||||
self.run()
|
self.run()
|
||||||
log.info("UcdSdk.send_current_pattern_params done")
|
log.info("UcdSdk.send_current_pattern_params done")
|
||||||
@@ -814,12 +925,7 @@ class _UcdSdkBackend:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log.info("UcdSdk.send_solid_rgb_pattern rgb=%s", rgb)
|
log.info("UcdSdk.send_solid_rgb_pattern rgb=%s", rgb)
|
||||||
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
|
return self.apply_solid_color(list(rgb))
|
||||||
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))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("UcdSdk.send_solid_rgb_pattern exception")
|
log.exception("UcdSdk.send_solid_rgb_pattern exception")
|
||||||
return False
|
return False
|
||||||
@@ -839,6 +945,20 @@ class _UcdSdkBackend:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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 ────────────────────────────────
|
# ─── §3 DeviceInfo / list_devices ────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -1244,6 +1364,13 @@ class UCD323Device(IUcdDevice):
|
|||||||
self._state = UcdState.APPLIED
|
self._state = UcdState.APPLIED
|
||||||
return ok
|
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:
|
def _apply_dynamic_range(self, signal: SignalFormat) -> None:
|
||||||
@@ -1262,6 +1389,13 @@ class UCD323Device(IUcdDevice):
|
|||||||
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
|
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
|
||||||
return bool(self._sdk.send_image_pattern(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()
|
# 预定义图案路径:复用 controller.set_pattern + run()
|
||||||
|
|
||||||
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
|
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
|
||||||
@@ -1270,11 +1404,7 @@ class UCD323Device(IUcdDevice):
|
|||||||
self._sdk.current_pattern = video_pattern
|
self._sdk.current_pattern = video_pattern
|
||||||
|
|
||||||
params: list[int] | None = None
|
params: list[int] | None = None
|
||||||
if pattern.kind is PatternKind.SOLID:
|
if pattern.extras:
|
||||||
if pattern.solid_rgb is None:
|
|
||||||
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
|
|
||||||
params = list(pattern.solid_rgb)
|
|
||||||
elif pattern.extras:
|
|
||||||
params = list(pattern.extras)
|
params = list(pattern.extras)
|
||||||
|
|
||||||
if not self._sdk.set_pattern(video_pattern, params):
|
if not self._sdk.set_pattern(video_pattern, params):
|
||||||
|
|||||||
111
app/ucd/pg_status.py
Normal file
111
app/ucd/pg_status.py
Normal 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)
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from app.ucd.domain import (
|
from app.ucd.domain import (
|
||||||
Colorimetry,
|
Colorimetry,
|
||||||
@@ -33,6 +34,21 @@ class SignalService:
|
|||||||
def __init__(self, device: IUcdDevice, bus: EventBus):
|
def __init__(self, device: IUcdDevice, bus: EventBus):
|
||||||
self._dev = device
|
self._dev = device
|
||||||
self._bus = bus
|
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()
|
self._dev.apply()
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def send_pattern(self, pattern: PatternSpec) -> None:
|
def send_pattern(
|
||||||
|
self,
|
||||||
|
pattern: PatternSpec,
|
||||||
|
*,
|
||||||
|
pattern_hint: str | None = None,
|
||||||
|
) -> None:
|
||||||
"""在已 configure 的信号上仅更新图案后 apply。"""
|
"""在已 configure 的信号上仅更新图案后 apply。"""
|
||||||
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
|
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
|
||||||
self._dev.set_pattern(pattern)
|
self._dev.set_pattern(pattern)
|
||||||
self._dev.apply()
|
self._dev.apply()
|
||||||
|
self._notify_pattern_sent(pattern_hint)
|
||||||
|
|
||||||
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
|
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:
|
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(
|
def update_signal_format(
|
||||||
self,
|
self,
|
||||||
@@ -127,9 +156,26 @@ class SignalService:
|
|||||||
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern(不 apply 输出)。"""
|
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern(不 apply 输出)。"""
|
||||||
return bool(self._dev.set_ucd_params(config))
|
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。"""
|
"""以 ``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:
|
def apply_and_run(self, config, pattern_params) -> bool:
|
||||||
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
|
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
|
||||||
@@ -380,7 +426,13 @@ class PatternService:
|
|||||||
raise IndexError(f"pattern 索引越界: {index}")
|
raise IndexError(f"pattern 索引越界: {index}")
|
||||||
|
|
||||||
pattern_param = session.pattern_params[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}")
|
raise RuntimeError(f"发送 pattern 失败: {index}")
|
||||||
return pattern_param
|
return pattern_param
|
||||||
|
|
||||||
|
|||||||
2
app/ucd/solid_color_scale.py
Normal file
2
app/ucd/solid_color_scale.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""兼容层:请改用 ``app.solid_color_scale``。"""
|
||||||
|
from app.solid_color_scale import * # noqa: F403
|
||||||
@@ -22,6 +22,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
from app.tests.color_accuracy import calculate_delta_e_2000
|
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
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -41,10 +47,15 @@ DE_FORMULAS = ["2000", "94", "76"]
|
|||||||
|
|
||||||
|
|
||||||
def _pct_to_gray_rgb(pct: int) -> tuple[int, int, int]:
|
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
|
return value, value, value
|
||||||
|
|
||||||
|
|
||||||
|
def _pct_to_pattern_rgb(pct: int) -> int:
|
||||||
|
"""灰阶百分比 → SolidColor 码值(0–65535)。"""
|
||||||
|
return pct_to_solid_color(pct)
|
||||||
|
|
||||||
|
|
||||||
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
def _rgb_to_hex(rgb: tuple[int, int, int]) -> str:
|
||||||
r, g, b = rgb
|
r, g, b = rgb
|
||||||
return f"#{r:02x}{g:02x}{b:02x}"
|
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_stop_event = threading.Event()
|
||||||
self.calman_running = False
|
self.calman_running = False
|
||||||
self.calman_patch_send_busy = 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_current_level = None
|
||||||
self.calman_last_record = None
|
self.calman_last_record = None
|
||||||
self.calman_last_step_seconds = 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:
|
def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
||||||
"""点击色块时,发送对应灰阶图案到信号发生器。"""
|
"""点击色块时,发送对应灰阶图案到信号发生器。"""
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
@@ -738,7 +774,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
|||||||
self.calman_status_var.set("发送进行中,请稍候...")
|
self.calman_status_var.set("发送进行中,请稍候...")
|
||||||
return
|
return
|
||||||
|
|
||||||
rgb_val = int(round(pct * 255 / 100))
|
rgb_val = _pct_to_pattern_rgb(pct)
|
||||||
self.calman_current_level = pct
|
self.calman_current_level = pct
|
||||||
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
||||||
_highlight_patch(self, pct)
|
_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}")
|
_calman_log(self, f"send_solid_rgb start pct={pct}")
|
||||||
test_type = getattr(self.config, "current_test_type", "screen_module")
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
if hasattr(self, "pattern_service") and self.pattern_service is not None:
|
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(
|
self.pattern_service.send_rgb(
|
||||||
(rgb_val, rgb_val, rgb_val),
|
(rgb_val, rgb_val, rgb_val),
|
||||||
|
session=rgb_session,
|
||||||
test_type=test_type,
|
test_type=test_type,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -902,11 +940,7 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
test_type = getattr(self.config, "current_test_type", "screen_module")
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
rgb_session = None
|
rgb_session = None
|
||||||
if hasattr(self, "pattern_service") and self.pattern_service is not None:
|
if hasattr(self, "pattern_service") and self.pattern_service is not None:
|
||||||
rgb_session = self.pattern_service.prepare_session(
|
rgb_session = _get_calman_rgb_session(self, test_type)
|
||||||
"rgb",
|
|
||||||
test_type=test_type,
|
|
||||||
log_details=False,
|
|
||||||
)
|
|
||||||
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
|
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
|
||||||
|
|
||||||
order = sorted(self.calman_levels, reverse=True)
|
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")
|
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
|
||||||
break
|
break
|
||||||
step_t0 = time.perf_counter()
|
step_t0 = time.perf_counter()
|
||||||
rgb_val = int(round(pct * 255 / 100))
|
rgb_val = _pct_to_pattern_rgb(pct)
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
|
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ from typing import TYPE_CHECKING
|
|||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
from app.pq import pq_config
|
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:
|
if TYPE_CHECKING:
|
||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
@@ -56,19 +62,21 @@ def _gray_pct_of(rgb) -> str:
|
|||||||
r = int(rgb[0])
|
r = int(rgb[0])
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
return str(int(round(r / 255 * 100)))
|
return str(int(round(solid_color_to_pct(r))))
|
||||||
|
|
||||||
|
|
||||||
def _hex_of(rgb) -> str:
|
def _hex_of(rgb) -> str:
|
||||||
try:
|
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:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
return f"#{r:02X}{g:02X}{b:02X}"
|
return f"#{r:02X}{g:02X}{b:02X}"
|
||||||
|
|
||||||
|
|
||||||
def _luminance(rgb) -> float:
|
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
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||||
|
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ def _gen_even(n: int) -> list[list[int]]:
|
|||||||
out = []
|
out = []
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
pct = 100.0 - (100.0 / (n - 1)) * i
|
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])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -118,7 +126,7 @@ def _gen_pq(n: int) -> list[list[int]]:
|
|||||||
else:
|
else:
|
||||||
Lm1 = L ** m1
|
Lm1 = L ** m1
|
||||||
v_pq = ((c1 + c2 * Lm1) / (1.0 + c3 * Lm1)) ** m2
|
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])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -129,7 +137,7 @@ def _gen_gamma(n: int, gamma: float = 2.2) -> list[list[int]]:
|
|||||||
out = []
|
out = []
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
lin = 1.0 - i / (n - 1)
|
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])
|
out.append([v, v, v])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -761,8 +769,8 @@ def _parse_rgb_from_form(self: "PQAutomationApp"):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
messagebox.showerror("输入错误", "R/G/B 必须为整数")
|
messagebox.showerror("输入错误", "R/G/B 必须为整数")
|
||||||
return None
|
return None
|
||||||
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)):
|
||||||
messagebox.showerror("输入错误", "R/G/B 必须在 0~255 范围内")
|
messagebox.showerror("输入错误", f"R/G/B 必须在 0~{SOLID_COLOR_MAX} 范围内")
|
||||||
return None
|
return None
|
||||||
return [r, g, b]
|
return [r, g, b]
|
||||||
|
|
||||||
@@ -775,7 +783,7 @@ def _fill_rgb_from_pct(self: "PQAutomationApp"):
|
|||||||
messagebox.showerror("输入错误", "灰度 % 必须为数字")
|
messagebox.showerror("输入错误", "灰度 % 必须为数字")
|
||||||
return
|
return
|
||||||
pct = max(0.0, min(100.0, pct))
|
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_r_var.set(str(v))
|
||||||
self._gamma_edit_g_var.set(str(v))
|
self._gamma_edit_g_var.set(str(v))
|
||||||
self._gamma_edit_b_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("%"):
|
if s.endswith("%"):
|
||||||
try:
|
try:
|
||||||
pct = float(s.rstrip("%"))
|
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])
|
rows.append([v, v, v])
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -914,7 +922,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
|
|||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
try:
|
try:
|
||||||
v = int(parts[0])
|
v = int(parts[0])
|
||||||
if 0 <= v <= 255:
|
if 0 <= v <= SOLID_COLOR_MAX:
|
||||||
rows.append([v, v, v])
|
rows.append([v, v, v])
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -929,7 +937,7 @@ def _paste_from_clipboard(self: "PQAutomationApp"):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
errors.append(f"第 {ln_no} 行非整数:{s}")
|
errors.append(f"第 {ln_no} 行非整数:{s}")
|
||||||
continue
|
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}")
|
errors.append(f"第 {ln_no} 行越界:{s}")
|
||||||
continue
|
continue
|
||||||
rows.append([r, g, b])
|
rows.append([r, g, b])
|
||||||
@@ -980,7 +988,7 @@ def _run_validation(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
bad = [
|
bad = [
|
||||||
i for i, rgb in enumerate(params)
|
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:
|
if bad:
|
||||||
msgs.append(f"⚠ 越界点 (索引): {bad}")
|
msgs.append(f"⚠ 越界点 (索引): {bad}")
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ import threading
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import ttkbootstrap as ttk
|
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:
|
if TYPE_CHECKING:
|
||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
@@ -196,8 +201,12 @@ def _load_patterns(self: "PQAutomationApp"):
|
|||||||
continue
|
continue
|
||||||
if r is None or g is None or b is None:
|
if r is None or g is None or b is None:
|
||||||
continue
|
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
|
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))
|
patterns.append((r, g, b))
|
||||||
finally:
|
finally:
|
||||||
wb.close()
|
wb.close()
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ lUniTAP = UniTAP.TsiLib()
|
|||||||
#
|
#
|
||||||
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
|
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
|
||||||
#
|
#
|
||||||
dev = lUniTAP.open("NNNNNNNN")
|
dev = lUniTAP.open(0)
|
||||||
|
|
||||||
# After opening device as in UCD Console device role should be selected.
|
# After opening device as in UCD Console device role should be selected.
|
||||||
role = dev.select_role(UniTAP.dev.UCD422.HDMISourceHDMISink)
|
# role = dev.select_role(UniTAP.dev.UCD422.HDMISourceHDMISink)
|
||||||
# role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
||||||
role.hdtx.sdpg.add_packet(bytearray([0x81, 0x01, 0x1B, 0x16, 0x8B, 0x84, 0x90, 0x40, 0x00, 0x00, 0x06, 0x64, 0x00, 0x00, 0x00, 0x00,
|
# role.hdtx.sdpg.add_packet(bytearray([0x81, 0x01, 0x1B, 0x16, 0x8B, 0x84, 0x90, 0x40, 0x00, 0x00, 0x06, 0x64, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
# 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00]))
|
# 0x00, 0x00, 0x00, 0x00]))
|
||||||
role.hdtx.sdpg.load_packet("PATH_TO_PACKET_FILE")
|
role.hdtx.sdpg.load_packet("UniTAP/dev/modules/dut_tests/cfg/hdr10/vsif1.bin")
|
||||||
|
|
||||||
role.hdtx.sdpg.stop()
|
role.hdtx.sdpg.stop()
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ lUniTAP = UniTAP.TsiLib()
|
|||||||
#
|
#
|
||||||
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
|
# For opening device, please, put serial number of the device as 8 symbol str or put index of device.
|
||||||
#
|
#
|
||||||
# dev = lUniTAP.open(0) # Open first device
|
dev = lUniTAP.open(0) # Open first device
|
||||||
dev = lUniTAP.open("NNNNNNNN") # Put your device serial number here
|
# dev = lUniTAP.open("NNNNNNNN") # Put your device serial number here
|
||||||
|
|
||||||
# After opening device as in UCD Console device role should be selected.
|
# After opening device as in UCD Console device role should be selected.
|
||||||
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
||||||
@@ -26,10 +26,12 @@ role.hdtx.pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
|
|||||||
role.hdtx.pg.set_vm(vm=video_mode)
|
role.hdtx.pg.set_vm(vm=video_mode)
|
||||||
role.hdtx.pg.apply()
|
role.hdtx.pg.apply()
|
||||||
|
|
||||||
for i in range(1024):
|
# for i in range(1024):
|
||||||
print(f"Set color: {hex(i)} {hex(i)} {hex(i)}")
|
# print(f"Set color: {hex(i)} {hex(i)} {hex(i)}")
|
||||||
role.hdtx.pg.set_pattern_params(pattern_params=UniTAP.SolidColorParams(i, i, i))
|
# role.hdtx.pg.set_pattern_params(pattern_params=UniTAP.SolidColorParams(i, i, i))
|
||||||
time.sleep(0.01)
|
# time.sleep(0.01)
|
||||||
|
print(f"Set color: {hex(0)} {hex(65535)} {hex(65535)}")
|
||||||
|
role.hdtx.pg.set_pattern_params(pattern_params=UniTAP.SolidColorParams(0, 65535, 65535))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Since the 3.5 version, TsiLib and TSIDevice objects have the option to be closed earlier.
|
# Since the 3.5 version, TsiLib and TSIDevice objects have the option to be closed earlier.
|
||||||
|
|||||||
1464
hdr10_demo.py
Normal file
1464
hdr10_demo.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -208,6 +208,7 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
# 创建日志显示区域
|
# 创建日志显示区域
|
||||||
self.create_log_panel()
|
self.create_log_panel()
|
||||||
|
self.signal_service.set_pg_status_log_callback(self._on_pg_status_log)
|
||||||
# 创建 Local Dimming 面板
|
# 创建 Local Dimming 面板
|
||||||
self.create_local_dimming_panel()
|
self.create_local_dimming_panel()
|
||||||
# 创建 AI 图片对话面板
|
# 创建 AI 图片对话面板
|
||||||
@@ -271,6 +272,9 @@ class PQAutomationApp(
|
|||||||
def on_connection_changed(evt: ConnectionChanged) -> None:
|
def on_connection_changed(evt: ConnectionChanged) -> None:
|
||||||
if evt.device is DeviceKind.UCD:
|
if evt.device is DeviceKind.UCD:
|
||||||
indicator = getattr(self, "ucd_status_indicator", None)
|
indicator = getattr(self, "ucd_status_indicator", None)
|
||||||
|
if not evt.connected:
|
||||||
|
from app.views.panels.calman_panel import _clear_calman_rgb_session
|
||||||
|
_clear_calman_rgb_session(self)
|
||||||
elif evt.device is DeviceKind.CA:
|
elif evt.device is DeviceKind.CA:
|
||||||
indicator = getattr(self, "ca_status_indicator", None)
|
indicator = getattr(self, "ca_status_indicator", None)
|
||||||
else:
|
else:
|
||||||
@@ -282,6 +286,14 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
self.event_bus.subscribe(ConnectionChanged, on_connection_changed)
|
self.event_bus.subscribe(ConnectionChanged, on_connection_changed)
|
||||||
|
|
||||||
|
def _on_pg_status_log(self, lines: list[str]) -> None:
|
||||||
|
"""每次 UCD 发图后,在 GUI 日志中打印 Pattern Generator 状态。"""
|
||||||
|
if not hasattr(self, "log_gui"):
|
||||||
|
return
|
||||||
|
self.log_gui.log("─ Pattern Generator Status ─", level="separator")
|
||||||
|
for line in lines:
|
||||||
|
self.log_gui.log(line, level="info")
|
||||||
|
|
||||||
def _dispatch_ui(self, fn, *args, **kwargs):
|
def _dispatch_ui(self, fn, *args, **kwargs):
|
||||||
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
|
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
"cct",
|
"cct",
|
||||||
"contrast"
|
"contrast"
|
||||||
],
|
],
|
||||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
"timing": "CTA 1920x 1080 @ 60Hz",
|
||||||
"data_range": "Limited",
|
"data_range": "Full",
|
||||||
"color_format": "RGB",
|
"color_format": "RGB",
|
||||||
"bpc": 8,
|
"bpc": 8,
|
||||||
"colorimetry": "DCI-P3",
|
"colorimetry": "DCI-P3",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
},
|
},
|
||||||
"device_config": {
|
"device_config": {
|
||||||
"ca_com": "COM3",
|
"ca_com": "COM3",
|
||||||
"ucd_list": "0: UCD-323 [2422C809]",
|
"ucd_list": "0: UCD-323 [2128C209]",
|
||||||
"ca_channel": "0"
|
"ca_channel": "0"
|
||||||
},
|
},
|
||||||
"custom_pattern": {
|
"custom_pattern": {
|
||||||
|
|||||||
BIN
temp_hdr10_images/pq_white_1920x1080_1000nits.png
Normal file
BIN
temp_hdr10_images/pq_white_1920x1080_1000nits.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
829
test.py
829
test.py
@@ -1,17 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
HDR10 信号输出验证脚本(UCD-323 HDMI Source + PG + sdpg DRM)。
|
HDR10 信号输出验证脚本(UCD-323 HDMI Source + SolidColor + sdpg)。
|
||||||
|
|
||||||
验证 PG(BT.2020 / 10bit / Full RGB)与 sdpg 自定义 DRM(0x87) InfoFrame 能否让 DUT 进入 HDR 模式。
|
通过 SolidColor 白场(0–65535,默认 65535)并配合 sdpg InfoFrame,
|
||||||
|
验证 DUT 能否进入 HDR 模式并输出峰值亮度。
|
||||||
|
|
||||||
用法(在项目根目录)::
|
用法(在项目根目录)::
|
||||||
|
|
||||||
python test.py
|
python test.py
|
||||||
python test.py --device 0
|
python test.py --device 0
|
||||||
python test.py --device ABCD1234 --max-cll 1000 --max-fall 400 --hold 60
|
python test.py --device ABCD1234 --max-cll 1000 --max-fall 400 --hold 60
|
||||||
|
python test.py --image path/to/white.png
|
||||||
python test.py --save-bin drm_hdr10.bin
|
python test.py --save-bin drm_hdr10.bin
|
||||||
|
|
||||||
验收:脚本发出 100% PQ 白场后,用 CA-410 测 DUT 峰值亮度。
|
验收:脚本发出 100% 白场后,用 CA-410 测 DUT 峰值亮度。
|
||||||
若仍约 100–200 nits(SDR 水平),说明信令或 DUT 设置未生效。
|
若仍约 100–200 nits(SDR 水平),说明信令或 DUT 设置未生效。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -19,20 +21,39 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from ctypes import c_uint32
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import UniTAP
|
from PIL import Image
|
||||||
|
|
||||||
# CTA-861 VIC 16 ≈ 3840×2160 @ 60Hz(设备需支持该 timing)
|
import UniTAP
|
||||||
DEFAULT_VIC = 16
|
import UniTAP.libs.lib_tsi.tsi_types as tsi_ci
|
||||||
|
|
||||||
|
from UniTAP.common.timing import Timing
|
||||||
|
|
||||||
|
from app.ucd.pg_status import read_pattern_generator_status_lines
|
||||||
|
|
||||||
|
# 与 pq_config 默认一致:DMT 1080p60(勿用 CTA VIC16 代替,时序参数不同)
|
||||||
|
DEFAULT_WIDTH = 1920
|
||||||
|
DEFAULT_HEIGHT = 1080
|
||||||
|
DEFAULT_REFRESH_HZ = 60.0
|
||||||
|
DEFAULT_STANDARD = "dmt"
|
||||||
DEFAULT_MAX_CLL = 1000
|
DEFAULT_MAX_CLL = 1000
|
||||||
DEFAULT_MAX_FALL = 400
|
DEFAULT_MAX_FALL = 400
|
||||||
DEFAULT_HOLD_SECONDS = 60
|
DEFAULT_HOLD_SECONDS = 60
|
||||||
# 10bit PQ 白场码值(与 work_with_solid_color.py 一致)
|
from app.solid_color_scale import SOLID_COLOR_MAX, SOLID_COLOR_MIN
|
||||||
PQ_WHITE_10BIT = 1023
|
SOLID_WHITE_PEAK = 65535
|
||||||
PACKET_PAD_BYTES = 36
|
PACKET_PAD_BYTES = 36
|
||||||
|
|
||||||
|
# work_with_packet_generation.py 内联 VSIF(HDR10+,type 0x81)
|
||||||
|
OFFICIAL_HDR10PLUS_VSIF = bytearray([
|
||||||
|
0x81, 0x01, 0x1B, 0x16, 0x8B, 0x84, 0x90, 0x40, 0x00, 0x00, 0x06, 0x64,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def xy_to_be16(value: float) -> tuple[int, int]:
|
def xy_to_be16(value: float) -> tuple[int, int]:
|
||||||
"""CIE xy → 大端 uint16(单位 0.00002)。"""
|
"""CIE xy → 大端 uint16(单位 0.00002)。"""
|
||||||
@@ -41,6 +62,33 @@ def xy_to_be16(value: float) -> tuple[int, int]:
|
|||||||
return (u >> 8) & 0xFF, u & 0xFF
|
return (u >> 8) & 0xFF, u & 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
def build_infoframe_packet(
|
||||||
|
frame_type: int,
|
||||||
|
version: int,
|
||||||
|
body: bytearray,
|
||||||
|
*,
|
||||||
|
pad_to: int = PACKET_PAD_BYTES,
|
||||||
|
) -> bytearray:
|
||||||
|
"""构造 HDMI InfoFrame 包(header + body + 可选 padding)。"""
|
||||||
|
header = bytearray([frame_type, version, len(body), 0x00])
|
||||||
|
header[3] = (256 - (sum(header[:3]) + sum(body)) % 256) & 0xFF
|
||||||
|
packet = header + body
|
||||||
|
if len(packet) < pad_to:
|
||||||
|
packet.extend(b"\x00" * (pad_to - len(packet)))
|
||||||
|
return packet
|
||||||
|
|
||||||
|
|
||||||
|
def build_avi_bt2020_packet(vic: int = 16, *, full_range: bool = True) -> bytearray:
|
||||||
|
"""AVI InfoFrame(0x82):RGB / BT.2020 / Full / 指定 VIC。"""
|
||||||
|
body = bytearray(13)
|
||||||
|
body[0] = 0x00 # RGB, no scan info
|
||||||
|
body[1] = (0x03 << 6) | (0x02 << 4) # Extended colorimetry, 16:9
|
||||||
|
q = 0x01 if full_range else 0x00
|
||||||
|
body[2] = (0x06 << 4) | (q << 2) # BT.2020, full range
|
||||||
|
body[3] = vic & 0x7F
|
||||||
|
return build_infoframe_packet(0x82, 0x02, body)
|
||||||
|
|
||||||
|
|
||||||
def build_drm_hdr10_packet(
|
def build_drm_hdr10_packet(
|
||||||
max_cll: int = DEFAULT_MAX_CLL,
|
max_cll: int = DEFAULT_MAX_CLL,
|
||||||
max_fall: int = DEFAULT_MAX_FALL,
|
max_fall: int = DEFAULT_MAX_FALL,
|
||||||
@@ -79,6 +127,39 @@ def build_drm_hdr10_packet(
|
|||||||
return packet
|
return packet
|
||||||
|
|
||||||
|
|
||||||
|
def build_sdpg_packet_list(
|
||||||
|
args,
|
||||||
|
*,
|
||||||
|
drm: bytearray,
|
||||||
|
vsif_path: Path,
|
||||||
|
vic: int,
|
||||||
|
) -> tuple[list[bytearray], str]:
|
||||||
|
"""组装 sdpg 待发 InfoFrame 列表(可多包)。"""
|
||||||
|
if args.packet in ("official-vsif", "vendor-vsif"):
|
||||||
|
if args.packet == "official-vsif":
|
||||||
|
data = bytearray(OFFICIAL_HDR10PLUS_VSIF)
|
||||||
|
label = "official VSIF HDR10+ (0x81, 同官方示例)"
|
||||||
|
else:
|
||||||
|
data = bytearray(vsif_path.read_bytes())
|
||||||
|
label = f"vendor VSIF (HDR10+) {vsif_path.name}"
|
||||||
|
return [data], label
|
||||||
|
|
||||||
|
packets: list[bytearray] = []
|
||||||
|
labels: list[str] = []
|
||||||
|
|
||||||
|
if args.packet == "full":
|
||||||
|
packets.append(bytearray(OFFICIAL_HDR10PLUS_VSIF))
|
||||||
|
labels.append("VSIF HDR10+ (0x81)")
|
||||||
|
|
||||||
|
if not args.no_avi:
|
||||||
|
packets.append(build_avi_bt2020_packet(vic))
|
||||||
|
labels.append(f"AVI BT.2020 VIC{vic}")
|
||||||
|
|
||||||
|
packets.append(drm)
|
||||||
|
labels.append("DRM HDR10 (0x87)")
|
||||||
|
return packets, " + ".join(labels)
|
||||||
|
|
||||||
|
|
||||||
def verify_drm_checksum(packet: bytearray) -> bool:
|
def verify_drm_checksum(packet: bytearray) -> bool:
|
||||||
"""校验 InfoFrame checksum(type + version + length + payload + checksum ≡ 0 mod 256)。"""
|
"""校验 InfoFrame checksum(type + version + length + payload + checksum ≡ 0 mod 256)。"""
|
||||||
if len(packet) < 30 or packet[0] != 0x87:
|
if len(packet) < 30 or packet[0] != 0x87:
|
||||||
@@ -99,42 +180,338 @@ def format_packet_hex(packet: bytes, bytes_per_line: int = 16) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def build_hdr10_video_mode(role, vic: int) -> UniTAP.VideoMode:
|
def resolve_timing(
|
||||||
timing_mgr = role.hdtx.pg.timing_manager
|
role,
|
||||||
timing = timing_mgr.get_cta(vic)
|
*,
|
||||||
if timing is None:
|
width: int = DEFAULT_WIDTH,
|
||||||
available = timing_mgr.get_all()
|
height: int = DEFAULT_HEIGHT,
|
||||||
|
refresh_hz: float = DEFAULT_REFRESH_HZ,
|
||||||
|
standard: str = DEFAULT_STANDARD,
|
||||||
|
vic: int | None = None,
|
||||||
|
) -> Timing:
|
||||||
|
"""按分辨率查找 timing;默认 DMT 1080p60(与主程序 pq_config 一致)。"""
|
||||||
|
mgr = role.hdtx.pg.timing_manager
|
||||||
|
f_rate = int(round(refresh_hz * 1000))
|
||||||
|
|
||||||
|
if vic is not None:
|
||||||
|
timing = mgr.get_cta(vic)
|
||||||
|
if timing is not None:
|
||||||
|
return timing
|
||||||
|
raise RuntimeError(f"设备不支持 CTA VIC={vic}")
|
||||||
|
|
||||||
|
std_map = {
|
||||||
|
"dmt": Timing.Standard.SD_DMT,
|
||||||
|
"cta": Timing.Standard.SD_CTA,
|
||||||
|
"cvt": Timing.Standard.SD_CVT,
|
||||||
|
}
|
||||||
|
std = std_map.get(standard.lower())
|
||||||
|
if std is None:
|
||||||
|
raise ValueError(f"未知 standard={standard},可选 dmt/cta/cvt")
|
||||||
|
|
||||||
|
timing = mgr.search(
|
||||||
|
h_active=width,
|
||||||
|
v_active=height,
|
||||||
|
f_rate=f_rate,
|
||||||
|
standard=std,
|
||||||
|
)
|
||||||
|
if timing is not None:
|
||||||
|
return timing
|
||||||
|
|
||||||
|
hint = mgr.print_all()
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"设备不支持 CTA VIC={vic}。"
|
f"未找到 timing: {standard.upper()} {width}x{height}@{refresh_hz}Hz\n"
|
||||||
f"可用 timing 数量: {len(available)},请尝试 --vic 其它值。"
|
f"可用列表:\n{hint}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_video_mode(
|
||||||
|
timing: Timing,
|
||||||
|
*,
|
||||||
|
content_mode: str = "sdr",
|
||||||
|
bpc: int = 10,
|
||||||
|
) -> UniTAP.VideoMode:
|
||||||
|
"""ImageFile 等自定义 pattern 用。"""
|
||||||
color = UniTAP.ColorInfo()
|
color = UniTAP.ColorInfo()
|
||||||
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
||||||
|
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
|
||||||
|
if content_mode == "hdr-content":
|
||||||
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
|
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
|
||||||
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA # Full
|
color.bpc = bpc
|
||||||
color.bpc = 10
|
else:
|
||||||
|
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
|
||||||
|
color.bpc = 8
|
||||||
return UniTAP.VideoMode(timing=timing, color_info=color)
|
return UniTAP.VideoMode(timing=timing, color_info=color)
|
||||||
|
|
||||||
|
|
||||||
def apply_pg_white(role, video_mode: UniTAP.VideoMode, white_code: int) -> None:
|
def build_solid_video_mode(
|
||||||
|
timing: Timing,
|
||||||
|
*,
|
||||||
|
content_mode: str = "sdr",
|
||||||
|
bpc: int = 10,
|
||||||
|
) -> UniTAP.VideoMode:
|
||||||
|
"""
|
||||||
|
SolidColor 专用 VideoMode,对齐 work_with_solid_color.py。
|
||||||
|
|
||||||
|
RGB SolidColor 硬件只接受 CM_sRGB(设 BT.709 会 WrongColorimetry);
|
||||||
|
码值 65535 须 bpc=10(PG 内部按 16-bpc 左移 16-bpc 位)。
|
||||||
|
"""
|
||||||
|
color = UniTAP.ColorInfo()
|
||||||
|
color.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
||||||
|
color.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
|
||||||
|
color.bpc = max(10, bpc)
|
||||||
|
if content_mode == "hdr-content":
|
||||||
|
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
|
||||||
|
else:
|
||||||
|
color.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
|
||||||
|
return UniTAP.VideoMode(timing=timing, color_info=color)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_content_mode(args) -> str:
|
||||||
|
"""PG 像素:full 包 + 非 pg-only 时默认 10bit;否则 SDR 8bit 保出图。"""
|
||||||
|
if args.safe:
|
||||||
|
return "sdr"
|
||||||
|
if args.hdr_content or args.sdr_content:
|
||||||
|
return "sdr" if args.sdr_content else "hdr-content"
|
||||||
|
if args.pg_only or args.infoframe_only:
|
||||||
|
return "sdr"
|
||||||
|
if args.packet == "full":
|
||||||
|
return "hdr-content"
|
||||||
|
return "sdr"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output_mode(args, content_mode: str) -> str:
|
||||||
|
"""默认 SolidColor(0–65535);Image 易触发 WrongColorimetry。"""
|
||||||
|
if args.no_pg:
|
||||||
|
return "none"
|
||||||
|
if args.output is not None:
|
||||||
|
return args.output
|
||||||
|
return "solid"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_white_code(args, *, content_mode: str, output_mode: str) -> int:
|
||||||
|
"""SolidColor 白场码值;Image 模式 SDR 仍映射到 8bit RGB。"""
|
||||||
|
code = max(0, min(args.white_code, SOLID_COLOR_MAX))
|
||||||
|
if output_mode == "image" and content_mode == "sdr":
|
||||||
|
return min(code, 255)
|
||||||
|
return code
|
||||||
|
|
||||||
|
|
||||||
|
def content_mode_label(mode: str, bpc: int, *, output_mode: str = "solid") -> str:
|
||||||
|
if mode == "hdr-content":
|
||||||
|
return f"BT.2020 {bpc}bit"
|
||||||
|
if output_mode == "solid":
|
||||||
|
return f"sRGB {max(10, bpc)}bit"
|
||||||
|
return "BT.709 8bit"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_audio(role) -> None:
|
||||||
|
try:
|
||||||
|
role.hdtx.ag.stop_generate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def read_pg_diagnostics(pg) -> dict:
|
||||||
|
st = pg.status()
|
||||||
|
return {
|
||||||
|
"pg_error": st.error.name,
|
||||||
|
"pg_error_str": str(st.error),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rgb_from_white_code(white_code: int, *, hdr_content: bool) -> tuple[int, int, int]:
|
||||||
|
"""将码值映射为 8bit RGB(ImageFile 经 UICL 按 VideoMode 转换)。"""
|
||||||
|
if not hdr_content:
|
||||||
|
level = max(0, min(255, white_code if white_code <= 255 else 255))
|
||||||
|
else:
|
||||||
|
level = max(0, min(255, int(round(white_code * 255 / SOLID_COLOR_MAX))))
|
||||||
|
return level, level, level
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_white_image(
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
*,
|
||||||
|
hdr_content: bool,
|
||||||
|
white_code: int,
|
||||||
|
image_path: str | None = None,
|
||||||
|
) -> Path:
|
||||||
|
"""返回用于发图的 PNG 路径;未指定 --image 时在临时目录生成全屏白场。"""
|
||||||
|
if image_path:
|
||||||
|
path = Path(image_path).expanduser().resolve()
|
||||||
|
if not path.is_file():
|
||||||
|
raise FileNotFoundError(f"找不到图片: {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
rgb = rgb_from_white_code(white_code, hdr_content=hdr_content)
|
||||||
|
temp_dir = Path(tempfile.gettempdir()) / "pq_hdr_test_images"
|
||||||
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
suffix = "hdr" if hdr_content else "sdr"
|
||||||
|
out = temp_dir / f"white_{width}x{height}_{suffix}_{rgb[0]:03d}.png"
|
||||||
|
Image.new("RGB", (width, height), rgb).save(out, format="PNG")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def print_pg_status(pg, hint: str | None = None) -> None:
|
||||||
|
for line in read_pattern_generator_status_lines(pg, pattern_hint=hint):
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_hpd(role, *, timeout: float = 10.0, interval: float = 0.5) -> bool:
|
||||||
|
"""等待 Sink HPD 置位。"""
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if role.hdtx.link.status.hpd_status:
|
||||||
|
return True
|
||||||
|
time.sleep(interval)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_device(role, *, reset_pg: bool = True) -> None:
|
||||||
|
"""打开设备后清理 sdpg/PG 残留状态(避免此前测试黑屏)。"""
|
||||||
|
try:
|
||||||
|
role.hdtx.sdpg.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if reset_pg:
|
||||||
|
try:
|
||||||
|
role.hdtx.pg.reset()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
stop_audio(role)
|
||||||
|
|
||||||
|
|
||||||
|
def read_edid_summary(role) -> dict:
|
||||||
|
edid = role.hdtx.edid
|
||||||
|
local = edid.read_i2c()
|
||||||
|
remote = edid.read_sbm(0)
|
||||||
|
return {"local_len": len(local), "remote_len": len(remote)}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_pg_solid(role, video_mode: UniTAP.VideoMode, white_code: int) -> dict:
|
||||||
|
"""
|
||||||
|
发 SolidColor 白场。顺序对齐 work_with_solid_color.py:
|
||||||
|
set_pattern → set_vm → apply() → set_pattern_params(0–65535)。
|
||||||
|
"""
|
||||||
pg = role.hdtx.pg
|
pg = role.hdtx.pg
|
||||||
|
code = max(0, min(white_code, SOLID_COLOR_MAX))
|
||||||
pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
|
pg.set_pattern(pattern=UniTAP.VideoPattern.SolidColor)
|
||||||
pg.set_vm(vm=video_mode)
|
pg.set_vm(vm=video_mode)
|
||||||
if not pg.apply():
|
if not pg.apply():
|
||||||
raise RuntimeError("Pattern Generator apply() 失败")
|
diag = read_pg_diagnostics(pg)
|
||||||
pg.set_pattern_params(
|
raise RuntimeError(
|
||||||
pattern_params=UniTAP.SolidColorParams(white_code, white_code, white_code)
|
f"SolidColor apply() 失败: {diag['pg_error_str']} ({diag['pg_error']})"
|
||||||
)
|
)
|
||||||
|
pg.set_pattern_params(
|
||||||
|
pattern_params=UniTAP.SolidColorParams(code, code, code)
|
||||||
|
)
|
||||||
|
stop_audio(role)
|
||||||
|
return read_pg_diagnostics(pg)
|
||||||
|
|
||||||
|
|
||||||
def start_sdpg(role, drm_packet: bytearray):
|
def bootstrap_sdr_white(role, timing: Timing, *, settle: float) -> None:
|
||||||
|
"""先用 sRGB 10bit SolidColor 建立链路,再切目标格式。"""
|
||||||
|
boot_vm = build_solid_video_mode(timing, content_mode="sdr")
|
||||||
|
print(f" [bootstrap] sRGB 10bit SolidColor 白场 (码值 {SOLID_WHITE_PEAK}) ...")
|
||||||
|
apply_pg_solid(role, boot_vm, SOLID_WHITE_PEAK)
|
||||||
|
time.sleep(settle)
|
||||||
|
link = read_link_diagnostics(role)
|
||||||
|
print(
|
||||||
|
f" [bootstrap] pg_error=OK, video={'On' if link['video'] else 'Off'}, "
|
||||||
|
f"TMDS_lock={link['tmds_locked']}"
|
||||||
|
)
|
||||||
|
print_pg_status(role.hdtx.pg, "Solid Color bootstrap")
|
||||||
|
|
||||||
|
|
||||||
|
def apply_pg_image(role, video_mode: UniTAP.VideoMode, image_path: Path) -> dict:
|
||||||
|
"""
|
||||||
|
通过 ImageFile 发图。顺序对齐 device.send_image_pattern:
|
||||||
|
set_vm → set_pattern(图片路径) → apply()。
|
||||||
|
"""
|
||||||
|
pg = role.hdtx.pg
|
||||||
|
pg.set_vm(vm=video_mode)
|
||||||
|
pg.set_pattern(pattern=str(image_path))
|
||||||
|
if not pg.apply():
|
||||||
|
diag = read_pg_diagnostics(pg)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Image apply() 失败: {diag['pg_error_str']} ({diag['pg_error']})"
|
||||||
|
)
|
||||||
|
stop_audio(role)
|
||||||
|
return read_pg_diagnostics(pg)
|
||||||
|
|
||||||
|
|
||||||
|
def read_sdpg_caps_raw(sdpg) -> int:
|
||||||
|
"""读取 TSI_PACKETGEN_CAPS(Python API 未封装)。"""
|
||||||
|
io = sdpg._PacketGenerator__io # noqa: SLF001 — 诊断用
|
||||||
|
return io.get(tsi_ci.TSI_PACKETGEN_CAPS_R, c_uint32)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def read_link_diagnostics(role) -> dict:
|
||||||
|
st = role.hdtx.link.status
|
||||||
|
locks = st.channel_lock
|
||||||
|
return {
|
||||||
|
"hpd": st.hpd_status,
|
||||||
|
"video": st.video_status,
|
||||||
|
"hdmi_mode": st.hdmi_mode.name,
|
||||||
|
"channel_lock": locks,
|
||||||
|
"tmds_locked": any(locks) if locks else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def poll_sdpg_status(sdpg, seconds: float = 3.0, interval: float = 0.5):
|
||||||
|
"""start() 后轮询 status;部分固件需数帧后才置位。"""
|
||||||
|
samples = []
|
||||||
|
deadline = time.monotonic() + seconds
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
samples.append(sdpg.status())
|
||||||
|
time.sleep(interval)
|
||||||
|
return samples
|
||||||
|
|
||||||
|
|
||||||
|
def start_sdpg(role, packets: list[bytearray], *, pre_stop_delay: float = 0.5) -> None:
|
||||||
|
"""
|
||||||
|
启动 sdpg。流程对齐 work_with_packet_generation.py:
|
||||||
|
reset → add_packet(s) → stop → 等待 → start。
|
||||||
|
"""
|
||||||
sdpg = role.hdtx.sdpg
|
sdpg = role.hdtx.sdpg
|
||||||
sdpg.reset()
|
sdpg.reset()
|
||||||
sdpg.add_packet(drm_packet)
|
for idx, packet in enumerate(packets):
|
||||||
|
sdpg.add_packet(packet)
|
||||||
|
print(
|
||||||
|
f" sdpg add_packet[{idx}]: type=0x{packet[0]:02X}, "
|
||||||
|
f"len={len(packet)} bytes"
|
||||||
|
)
|
||||||
|
sdpg.stop()
|
||||||
|
print(f" sdpg stop 后: {sdpg.status()}")
|
||||||
|
if pre_stop_delay > 0:
|
||||||
|
time.sleep(pre_stop_delay)
|
||||||
sdpg.start()
|
sdpg.start()
|
||||||
return sdpg.status()
|
|
||||||
|
|
||||||
|
def interpret_sdpg_status(status, link: dict) -> list[str]:
|
||||||
|
"""根据 status / 链路状态给出可读结论。"""
|
||||||
|
notes = []
|
||||||
|
notes.append(
|
||||||
|
"sdpg 通过 add_packet/load_packet 发送自定义 InfoFrame(见官方 work_with_packet_generation.py);"
|
||||||
|
"caps 寄存器为 0 时仍可发送。"
|
||||||
|
)
|
||||||
|
if not link["hpd"]:
|
||||||
|
notes.append(
|
||||||
|
"HPD=Low:未检测到 Sink 热插拔。InfoFrame 可能不会在链路上送出,"
|
||||||
|
"请确认 HDMI 已接 DUT 且电视已开机。"
|
||||||
|
)
|
||||||
|
if not link["video"]:
|
||||||
|
notes.append("video_status=False:TX 视频未 enable,请先确认 Image 已有输出。")
|
||||||
|
if not status.error and not status.enabled and not status.active:
|
||||||
|
notes.append(
|
||||||
|
"start() 后 Enabled/Active 仍为 False:UCD-323 固件常如此,"
|
||||||
|
"包仍可能已在链路上;请以电视 HDR 状态或 CA-410 nits 为准。"
|
||||||
|
)
|
||||||
|
elif status.error:
|
||||||
|
notes.append("sdpg error=True:包格式或长度可能不被接受,可尝试 --packet vendor-vsif 对比。")
|
||||||
|
elif status.enabled or status.active:
|
||||||
|
notes.append("sdpg 已 Enabled/Active,信令发生器运行中。")
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
def parse_device_arg(value: str):
|
def parse_device_arg(value: str):
|
||||||
@@ -159,21 +536,31 @@ def list_devices(tsi: UniTAP.TsiLib) -> None:
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="UCD-323 HDR10 信号输出验证(PG + sdpg DRM InfoFrame)"
|
description="UCD-323 HDR10 信号输出验证(SolidColor 0–65535 + sdpg InfoFrame)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--device",
|
"--device",
|
||||||
default="0",
|
default="0",
|
||||||
help="设备序号(整数)或 8 位序列号,默认 0",
|
help="设备序号(整数)或 8 位序列号,默认 0",
|
||||||
)
|
)
|
||||||
parser.add_argument("--vic", type=int, default=DEFAULT_VIC, help=f"CTA VIC,默认 {DEFAULT_VIC}")
|
parser.add_argument("--vic", type=int, default=None, help="改用 CTA VIC(指定后忽略 --standard/--width 等)")
|
||||||
|
parser.add_argument("--standard", default=DEFAULT_STANDARD, choices=("dmt", "cta", "cvt"))
|
||||||
|
parser.add_argument("--width", type=int, default=DEFAULT_WIDTH)
|
||||||
|
parser.add_argument("--height", type=int, default=DEFAULT_HEIGHT)
|
||||||
|
parser.add_argument("--refresh", type=float, default=DEFAULT_REFRESH_HZ, help="Hz")
|
||||||
|
parser.add_argument("--settle", type=float, default=2.0, help="apply 后等待秒数,默认 2")
|
||||||
parser.add_argument("--max-cll", type=int, default=DEFAULT_MAX_CLL, help="DRM MaxCLL (nits)")
|
parser.add_argument("--max-cll", type=int, default=DEFAULT_MAX_CLL, help="DRM MaxCLL (nits)")
|
||||||
parser.add_argument("--max-fall", type=int, default=DEFAULT_MAX_FALL, help="DRM MaxFALL (nits)")
|
parser.add_argument("--max-fall", type=int, default=DEFAULT_MAX_FALL, help="DRM MaxFALL (nits)")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--white-code",
|
"--white-code",
|
||||||
type=int,
|
type=int,
|
||||||
default=PQ_WHITE_10BIT,
|
default=SOLID_WHITE_PEAK,
|
||||||
help=f"PQ 10bit 白场码值,默认 {PQ_WHITE_10BIT}",
|
help=f"SolidColor 白场 RGB 码值 0–65535,默认 {SOLID_WHITE_PEAK}(峰值白)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--image",
|
||||||
|
metavar="PATH",
|
||||||
|
help="发图文件路径(png/bmp/jpeg 等);省略则按分辨率自动生成白场 PNG",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--hold",
|
"--hold",
|
||||||
@@ -187,6 +574,119 @@ def main() -> int:
|
|||||||
help="将 DRM 包保存为 .bin 文件(便于 sdpg.load_packet 调试)",
|
help="将 DRM 包保存为 .bin 文件(便于 sdpg.load_packet 调试)",
|
||||||
)
|
)
|
||||||
parser.add_argument("--list-devices", action="store_true", help="列出设备后退出")
|
parser.add_argument("--list-devices", action="store_true", help="列出设备后退出")
|
||||||
|
parser.add_argument("--list-timings", action="store_true", help="列出设备 timing 后退出")
|
||||||
|
parser.add_argument(
|
||||||
|
"--packet",
|
||||||
|
choices=("full", "official-vsif", "drm", "vendor-vsif"),
|
||||||
|
default="full",
|
||||||
|
help="sdpg:full=VSIF+AVI+DRM(默认);official-vsif=仅官方 VSIF;drm=AVI+DRM",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-avi",
|
||||||
|
action="store_true",
|
||||||
|
help="drm 模式仅发 DRM、不发 AVI(对比测试用)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sdpg-before-pg",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="先 sdpg 再出图",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pg-before-sdpg",
|
||||||
|
action="store_false",
|
||||||
|
dest="sdpg_before_pg",
|
||||||
|
help="先出图再 sdpg(默认)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sdpg-reset",
|
||||||
|
action="store_true",
|
||||||
|
help="已废弃:start_sdpg 每次都会 reset()",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sdpg-delay",
|
||||||
|
type=float,
|
||||||
|
default=5.0,
|
||||||
|
help="sdpg stop 与 start 等待秒数(官方 5s),默认 5",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pg-only",
|
||||||
|
action="store_true",
|
||||||
|
help="仅 PG 出图,不发 sdpg",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--infoframe-only",
|
||||||
|
action="store_true",
|
||||||
|
help="SDR 白场 + 仅测 sdpg(不切 HDR 像素;默认仍出图,建议加 --vic 16)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-pg",
|
||||||
|
action="store_true",
|
||||||
|
help="完全不出图,仅 sdpg(复刻官方 work_with_packet_generation.py,电视通常黑屏)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-sdpg",
|
||||||
|
action="store_true",
|
||||||
|
help="已废弃:sdpg 默认始终发送(除非 --pg-only)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--safe",
|
||||||
|
action="store_true",
|
||||||
|
help="同默认 SDR 模式(8bit BT.709),显式指定时使用",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hdr-content",
|
||||||
|
action="store_true",
|
||||||
|
help="强制 PG 10bit BT.2020(--packet full 已默认)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sdr-content",
|
||||||
|
action="store_true",
|
||||||
|
help="强制 PG 8bit SDR(只测电视 HDR 标志,亮度不会升高)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
choices=("image", "solid"),
|
||||||
|
default=None,
|
||||||
|
help="白场输出:默认 SolidColor(推荐);image=ImageFile(可能 WrongColorimetry)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bootstrap",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="solid 输出前先用 SolidColor 建链(UCD-323 上易导致闪屏后黑屏,默认关闭)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-bootstrap",
|
||||||
|
action="store_false",
|
||||||
|
dest="bootstrap",
|
||||||
|
help="同默认行为:跳过 bootstrap",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--reset-pg",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="打开设备后 reset PG 并 stop sdpg(默认开启)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-reset-pg",
|
||||||
|
action="store_false",
|
||||||
|
dest="reset_pg",
|
||||||
|
help="不 reset PG",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hpd-wait",
|
||||||
|
type=float,
|
||||||
|
default=10.0,
|
||||||
|
help="等待 HPD 置位的超时秒数,默认 10",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bpc",
|
||||||
|
type=int,
|
||||||
|
choices=(8, 10, 12),
|
||||||
|
default=10,
|
||||||
|
help="HDR 内容色深(非 --safe / 非 --pg-only SDR 时),默认 10",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
tsi = UniTAP.TsiLib()
|
tsi = UniTAP.TsiLib()
|
||||||
@@ -195,62 +695,291 @@ def main() -> int:
|
|||||||
tsi.cleanup()
|
tsi.cleanup()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
if args.list_timings:
|
||||||
|
dev = tsi.open(parse_device_arg(args.device))
|
||||||
|
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
||||||
|
print(role.hdtx.pg.timing_manager.print_all())
|
||||||
|
tsi.close(dev)
|
||||||
|
tsi.cleanup()
|
||||||
|
return 0
|
||||||
|
|
||||||
drm = build_drm_hdr10_packet(max_cll=args.max_cll, max_fall=args.max_fall)
|
drm = build_drm_hdr10_packet(max_cll=args.max_cll, max_fall=args.max_fall)
|
||||||
if not verify_drm_checksum(drm):
|
if not verify_drm_checksum(drm):
|
||||||
print("错误: DRM 包 checksum 校验失败。", file=sys.stderr)
|
print("错误: DRM 包 checksum 校验失败。", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
vsif_path = (
|
||||||
|
Path(__file__).resolve().parent
|
||||||
|
/ "UniTAP/dev/modules/dut_tests/cfg/hdr10/vsif1.bin"
|
||||||
|
)
|
||||||
|
if args.packet == "vendor-vsif" and not vsif_path.is_file():
|
||||||
|
print(f"错误: 找不到 {vsif_path}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
if args.save_bin:
|
if args.save_bin:
|
||||||
out = Path(args.save_bin)
|
out = Path(args.save_bin)
|
||||||
out.write_bytes(drm)
|
out.write_bytes(drm)
|
||||||
print(f"已保存 DRM 包: {out.resolve()} ({len(drm)} bytes)")
|
print(f"已保存 DRM 包: {out.resolve()} ({len(drm)} bytes)")
|
||||||
|
|
||||||
|
preview_mode = resolve_content_mode(args)
|
||||||
|
output_mode = resolve_output_mode(args, preview_mode)
|
||||||
|
white_code = resolve_white_code(
|
||||||
|
args, content_mode=preview_mode, output_mode=output_mode
|
||||||
|
)
|
||||||
|
color_desc = content_mode_label(preview_mode, args.bpc, output_mode=output_mode)
|
||||||
|
preview_vic = args.vic if args.vic is not None else 16
|
||||||
|
preview_packets, preview_label = build_sdpg_packet_list(
|
||||||
|
args, drm=drm, vsif_path=vsif_path, vic=preview_vic
|
||||||
|
)
|
||||||
|
|
||||||
print("=== HDR10 信号输出测试 ===")
|
print("=== HDR10 信号输出测试 ===")
|
||||||
|
print("提示: 运行前请关闭 pqAutomationApp,避免 UCD 设备被占用。")
|
||||||
|
if args.infoframe_only:
|
||||||
|
print("模式: SDR 白场 + 仅 sdpg InfoFrame(观察电视 HDR 标志,亮度不会升高)")
|
||||||
|
elif args.no_pg:
|
||||||
|
print("模式: 无 PG 出图,仅 sdpg(官方示例,电视通常无画面)")
|
||||||
|
elif args.pg_only:
|
||||||
|
print("模式: 仅出图(跳过 sdpg InfoFrame)")
|
||||||
|
else:
|
||||||
|
print(f"模式: {color_desc} + sdpg ({preview_label})")
|
||||||
|
output_desc = {
|
||||||
|
"solid": "SolidColor",
|
||||||
|
"image": "ImageFile",
|
||||||
|
"none": "无(--no-pg)",
|
||||||
|
}[output_mode]
|
||||||
|
print(f"输出: {output_desc}" + (" + bootstrap SDR" if args.bootstrap else ""))
|
||||||
|
if args.infoframe_only:
|
||||||
|
print(
|
||||||
|
" 说明: 仍会输出 SDR 8bit 白场;只验证 sdpg 是否让电视进 HDR,亮度不会升高。"
|
||||||
|
" 测峰值亮度: python test.py --vic 16 --sdpg-delay 5"
|
||||||
|
)
|
||||||
|
if args.vic is None:
|
||||||
|
print(" 提示: 建议加 --vic 16(与 --pg-only 成功出图时序一致)。")
|
||||||
|
if preview_mode == "sdr" and not args.pg_only and not args.infoframe_only:
|
||||||
|
print(
|
||||||
|
" 注意: SDR 8bit 像素 + InfoFrame 通常不会提高亮度(CA-410 仍 ~100–200 nits)。"
|
||||||
|
" 要测峰值亮度请用默认 --packet full(10bit SolidColor + VSIF/AVI/DRM)。"
|
||||||
|
)
|
||||||
|
elif preview_mode == "hdr-content":
|
||||||
|
print(
|
||||||
|
f" 策略: SolidColor 码值 {white_code}(0–65535 峰值)+ sdpg 三包。"
|
||||||
|
" 若黑屏可试 --sdr-content 或 --pg-only --vic 16。"
|
||||||
|
)
|
||||||
|
if not args.pg_only and not args.no_pg:
|
||||||
|
print(f"sdpg: {preview_label}")
|
||||||
|
if args.packet in ("full", "drm"):
|
||||||
print(f"DRM MaxCLL={args.max_cll}, MaxFALL={args.max_fall}")
|
print(f"DRM MaxCLL={args.max_cll}, MaxFALL={args.max_fall}")
|
||||||
print(f"CTA VIC={args.vic}, bpc=10, BT.2020 RGB Full, PQ 白场码值={args.white_code}")
|
timing_desc = (
|
||||||
print("DRM 包内容:")
|
f"CTA VIC {args.vic}" if args.vic is not None
|
||||||
print(format_packet_hex(drm))
|
else f"{args.standard.upper()} {args.width}x{args.height}@{args.refresh}Hz"
|
||||||
|
)
|
||||||
|
white_desc = (
|
||||||
|
f"SolidColor 码值 {white_code}"
|
||||||
|
if output_mode == "solid"
|
||||||
|
else (args.image or f"白场 PNG (white_code={white_code})")
|
||||||
|
)
|
||||||
|
print(f"Timing: {timing_desc}, PG={color_desc} RGB Full, 内容={white_desc}, settle={args.settle}s")
|
||||||
|
if not args.pg_only:
|
||||||
|
for idx, pkt in enumerate(preview_packets):
|
||||||
|
print(f"sdpg 包[{idx}] (type=0x{pkt[0]:02X}):")
|
||||||
|
print(format_packet_hex(pkt))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
dev = None
|
dev = None
|
||||||
role = None
|
role = None
|
||||||
|
sdpg_started = False
|
||||||
try:
|
try:
|
||||||
device_id = parse_device_arg(args.device)
|
device_id = parse_device_arg(args.device)
|
||||||
print(f"正在打开设备: {device_id!r} ...")
|
print(f"正在打开设备: {device_id!r} ...")
|
||||||
|
try:
|
||||||
|
tsi.rescan_devices()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f" 警告: rescan_devices 失败: {exc}", file=sys.stderr)
|
||||||
dev = tsi.open(device_id)
|
dev = tsi.open(device_id)
|
||||||
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
role = dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
||||||
|
prepare_device(role, reset_pg=args.reset_pg)
|
||||||
|
|
||||||
print("配置 Pattern Generator ...")
|
if not wait_for_hpd(role, timeout=args.hpd_wait):
|
||||||
vm = build_hdr10_video_mode(role, args.vic)
|
print(
|
||||||
t = vm.timing
|
f" 警告: {args.hpd_wait:g}s 内 HPD 仍为 Low,"
|
||||||
refresh_hz = t.frame_rate / 1000.0 if t.frame_rate else 0.0
|
"请确认电视已开机且 HDMI 已连接。",
|
||||||
print(f" Timing: {t.hactive}x{t.vactive} @ {refresh_hz:g} Hz")
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
edid_info = read_edid_summary(role)
|
||||||
|
print(
|
||||||
|
f" EDID: local={edid_info['local_len']}B, "
|
||||||
|
f"remote(DUT)={edid_info['remote_len']}B"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("配置输出 ...")
|
||||||
|
timing = resolve_timing(
|
||||||
|
role,
|
||||||
|
width=args.width,
|
||||||
|
height=args.height,
|
||||||
|
refresh_hz=args.refresh,
|
||||||
|
standard=args.standard,
|
||||||
|
vic=args.vic,
|
||||||
|
)
|
||||||
|
sdpg = role.hdtx.sdpg
|
||||||
|
caps = read_sdpg_caps_raw(sdpg)
|
||||||
|
print(f" sdpg caps (raw): 0x{caps:08X}")
|
||||||
|
|
||||||
|
content_mode = resolve_content_mode(args)
|
||||||
|
output_mode = resolve_output_mode(args, content_mode)
|
||||||
|
white_code = resolve_white_code(
|
||||||
|
args, content_mode=content_mode, output_mode=output_mode
|
||||||
|
)
|
||||||
|
avi_vic = args.vic if args.vic is not None else timing.id
|
||||||
|
sdpg_packets, packet_label = build_sdpg_packet_list(
|
||||||
|
args, drm=drm, vsif_path=vsif_path, vic=avi_vic
|
||||||
|
)
|
||||||
|
if args.infoframe_only:
|
||||||
|
print(" 内容格式: sRGB 10bit SolidColor + sdpg(像素 SDR,信令由 sdpg 声明 HDR)。")
|
||||||
|
elif args.no_pg:
|
||||||
|
print(" 内容格式: 不出图,仅 sdpg。")
|
||||||
|
elif content_mode == "hdr-content":
|
||||||
|
print(
|
||||||
|
f" 内容格式: PG 10bit BT.2020 SolidColor 码值 {white_code}(0–65535)。"
|
||||||
|
)
|
||||||
|
elif not args.pg_only:
|
||||||
|
print(" 内容格式: PG 8bit BT.709 + sdpg(亮度不会升高,仅验证信令)。")
|
||||||
|
else:
|
||||||
|
print(" 内容格式: SDR 8bit BT.709。")
|
||||||
|
|
||||||
|
vm = (
|
||||||
|
build_solid_video_mode(timing, content_mode=content_mode, bpc=args.bpc)
|
||||||
|
if output_mode == "solid"
|
||||||
|
else build_video_mode(timing, content_mode=content_mode, bpc=args.bpc)
|
||||||
|
)
|
||||||
|
hdr_content = content_mode == "hdr-content"
|
||||||
|
image_path = None
|
||||||
|
if output_mode == "image":
|
||||||
|
image_path = ensure_white_image(
|
||||||
|
timing.hactive,
|
||||||
|
timing.vactive,
|
||||||
|
hdr_content=hdr_content,
|
||||||
|
white_code=white_code,
|
||||||
|
image_path=args.image,
|
||||||
|
)
|
||||||
|
print(f" Image: {image_path}")
|
||||||
|
elif output_mode == "none":
|
||||||
|
print(" Image: (跳过,--no-pg)")
|
||||||
|
print(f" 内容格式: {content_mode_label(content_mode, args.bpc, output_mode=output_mode)}")
|
||||||
|
refresh_hz = timing.frame_rate / 1000.0 if timing.frame_rate else 0.0
|
||||||
|
std_name = timing.standard.name.replace("SD_", "")
|
||||||
|
print(
|
||||||
|
f" Timing: {std_name} {timing.hactive}x{timing.vactive} @ {refresh_hz:g} Hz"
|
||||||
|
f" (id={timing.id})"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f" Color: {vm.color_info.colorimetry.name}, "
|
f" Color: {vm.color_info.colorimetry.name}, "
|
||||||
f"bpc={vm.color_info.bpc}, range={vm.color_info.dynamic_range.name}"
|
f"bpc={vm.color_info.bpc}, range={vm.color_info.dynamic_range.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
apply_pg_white(role, vm, args.white_code)
|
link = read_link_diagnostics(role)
|
||||||
print(" PG apply() 成功,已输出 PQ 白场。")
|
print(
|
||||||
|
f" 链路: HPD={'High' if link['hpd'] else 'Low'}, "
|
||||||
|
f"video={'On' if link['video'] else 'Off'}, "
|
||||||
|
f"TMDS_lock={link['tmds_locked']}, locks={link['channel_lock']}, "
|
||||||
|
f"mode={link['hdmi_mode']}"
|
||||||
|
)
|
||||||
|
if link["hpd"] and not link["tmds_locked"]:
|
||||||
|
print(
|
||||||
|
" 提示: UCD-323 TX 侧 TMDS lock 位常不可靠;"
|
||||||
|
"请以电视是否实际有画面为准(video=On 即可继续)。"
|
||||||
|
)
|
||||||
|
|
||||||
print("启动 sdpg (DRM InfoFrame) ...")
|
def run_output():
|
||||||
status = start_sdpg(role, drm)
|
if output_mode == "none":
|
||||||
print(f" sdpg status: {status}")
|
return
|
||||||
if status.error:
|
if args.bootstrap:
|
||||||
print(" 警告: sdpg 报告 error=True,请检查固件或包格式。", file=sys.stderr)
|
if output_mode == "image":
|
||||||
if not status.active and not status.enabled:
|
print(
|
||||||
print(" 警告: sdpg 未 active/enabled。", file=sys.stderr)
|
" 跳过 bootstrap:Image 模式直接出图即可;"
|
||||||
|
"SolidColor 建链在本机上会闪屏后黑屏。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bootstrap_sdr_white(role, timing, settle=args.settle)
|
||||||
|
if output_mode == "solid":
|
||||||
|
pg_diag = apply_pg_solid(role, vm, white_code)
|
||||||
|
hint = f"Solid Color {white_code} (0x{white_code:X})"
|
||||||
|
else:
|
||||||
|
pg_diag = apply_pg_image(role, vm, image_path)
|
||||||
|
hint = image_path.name
|
||||||
|
time.sleep(args.settle)
|
||||||
|
link_after = read_link_diagnostics(role)
|
||||||
|
print(
|
||||||
|
f" {output_mode} apply() OK, pg_error={pg_diag['pg_error']}, "
|
||||||
|
f"video={'On' if link_after['video'] else 'Off'}, "
|
||||||
|
f"TMDS_lock={link_after['tmds_locked']}"
|
||||||
|
)
|
||||||
|
if pg_diag["pg_error"] != "OK":
|
||||||
|
print(f" 警告: PG 状态异常 — {pg_diag['pg_error_str']}", file=sys.stderr)
|
||||||
|
print_pg_status(role.hdtx.pg, hint)
|
||||||
|
vm_rb = role.hdtx.pg.get_stream_video_mode()
|
||||||
|
print(
|
||||||
|
f" PG 读回: {vm_rb.timing.hactive}x{vm_rb.timing.vactive}, "
|
||||||
|
f"bpc={vm_rb.color_info.bpc}, cm={vm_rb.color_info.colorimetry.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_sdpg(*, reapply_pg: bool = True) -> bool:
|
||||||
|
nonlocal sdpg_started
|
||||||
|
if args.pg_only:
|
||||||
|
print(" 跳过 sdpg(--pg-only)。")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"启动 sdpg ({packet_label}) ...")
|
||||||
|
start_sdpg(role, sdpg_packets, pre_stop_delay=args.sdpg_delay)
|
||||||
|
sdpg_started = True
|
||||||
|
samples = poll_sdpg_status(sdpg, seconds=1.0, interval=0.25)
|
||||||
|
status = samples[-1]
|
||||||
|
print(f" sdpg status (start 后): {status}")
|
||||||
|
link_mid = read_link_diagnostics(role)
|
||||||
|
print(
|
||||||
|
f" sdpg 后链路: video={'On' if link_mid['video'] else 'Off'}"
|
||||||
|
f"(若为 Off,将重新 apply 出图)"
|
||||||
|
)
|
||||||
|
for note in interpret_sdpg_status(status, link_mid):
|
||||||
|
print(f" → {note}")
|
||||||
|
|
||||||
|
if reapply_pg and output_mode != "none":
|
||||||
|
print(" sdpg 后再次 apply 出图,保持画面 ...")
|
||||||
|
run_output()
|
||||||
|
return True
|
||||||
|
|
||||||
|
if args.no_pg:
|
||||||
|
run_sdpg(reapply_pg=False)
|
||||||
|
elif args.infoframe_only or args.pg_only:
|
||||||
|
run_output()
|
||||||
|
if args.infoframe_only:
|
||||||
|
run_sdpg(reapply_pg=True)
|
||||||
|
elif args.sdpg_before_pg:
|
||||||
|
run_sdpg(reapply_pg=False)
|
||||||
|
run_output()
|
||||||
|
else:
|
||||||
|
run_output()
|
||||||
|
run_sdpg(reapply_pg=True)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("--- 请用 CA-410 测量 DUT 100% 白场峰值亮度 ---")
|
print("--- 请用 CA-410 测量 DUT 100% 白场峰值亮度 ---")
|
||||||
print(" HDR 生效: 通常显著高于 SDR(具体取决于电视峰值)")
|
if preview_mode == "hdr-content":
|
||||||
print(" 仍为 ~100–200 nits: 检查 DUT HDR 开关 / HDMI 口 / 信令")
|
print(" HDR 生效: 应显著高于 SDR(电视峰值通常 400–1000+ nits)")
|
||||||
|
else:
|
||||||
|
print(" 当前为 SDR 像素: 亮度约 100–200 nits 属正常,不代表 sdpg 无效")
|
||||||
|
print(" 对比: 先跑 --pg-only --vic 16 记 SDR 基准")
|
||||||
print(f"--- 保持输出 {args.hold:.0f} 秒,Ctrl+C 可提前结束 ---")
|
print(f"--- 保持输出 {args.hold:.0f} 秒,Ctrl+C 可提前结束 ---")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
deadline = time.monotonic() + args.hold
|
deadline = time.monotonic() + args.hold
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
time.sleep(1.0)
|
time.sleep(2.0)
|
||||||
|
link_hold = read_link_diagnostics(role)
|
||||||
|
if not link_hold["video"] and output_mode != "none":
|
||||||
|
print(" [watchdog] video=Off,重新 apply 输出 ...")
|
||||||
|
if output_mode == "solid":
|
||||||
|
apply_pg_solid(role, vm, white_code)
|
||||||
|
else:
|
||||||
|
apply_pg_image(role, vm, image_path)
|
||||||
|
|
||||||
print("测试完成。")
|
print("测试完成。")
|
||||||
return 0
|
return 0
|
||||||
@@ -263,7 +992,7 @@ def main() -> int:
|
|||||||
list_devices(tsi)
|
list_devices(tsi)
|
||||||
return 1
|
return 1
|
||||||
finally:
|
finally:
|
||||||
if role is not None:
|
if role is not None and sdpg_started:
|
||||||
try:
|
try:
|
||||||
role.hdtx.sdpg.stop()
|
role.hdtx.sdpg.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
Reference in New Issue
Block a user