Files
pqAutomationApp/app/pq/pq_config.py
2026-05-19 11:50:53 +08:00

482 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PQ自动化测试配置模块
import json
import copy
from pathlib import Path
# =============================================================================
# Pattern 文件读写工具统一存储格式settings/patterns/{name}.json
# =============================================================================
_PATTERNS_DIR = Path("settings/patterns")
def load_pattern_file(filepath) -> dict:
"""
从 JSON 文件加载 pattern 配置。
文件格式::
{
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": N,
"pattern_params": [[R, G, B], ...]
}
"""
with open(filepath, encoding="utf-8") as f:
return json.load(f)
def save_pattern_file(filepath, pattern: dict) -> None:
"""将 pattern 配置保存到 JSON 文件(目录不存在时自动创建)。"""
path = Path(filepath)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(pattern, f, indent=4, ensure_ascii=False)
def _load_pattern_or_empty(filepath, default=None) -> dict:
"""加载 pattern 文件;文件不存在或格式错误时返回 default或空 pattern 结构)。"""
try:
return load_pattern_file(filepath)
except (FileNotFoundError, json.JSONDecodeError, KeyError):
if default is not None:
return copy.deepcopy(default)
return {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 0,
"pattern_params": [],
}
# =============================================================================
# 静态默认值 —— 纯常量,永不在运行时修改
# =============================================================================
_DEFAULT_CCT_PARAMS = {
"screen_module": {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
},
"sdr_movie": {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
},
"hdr_movie": {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
},
}
_DEFAULT_GAMUT_REFERENCE = {
"screen_module": "DCI-P3",
"sdr_movie": "BT.709",
"hdr_movie": "BT.2020",
}
_DEFAULT_TEST_TYPES = {
"screen_module": {
"name": "屏模组性能测试",
"test_items": ["gamut", "gamma", "cct", "contrast"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
"patterns": {"gamut": "rgb", "gamma": "gray", "cct": "gray", "contrast": "rgb"},
},
"sdr_movie": {
"name": "SDR Movie测试",
"test_items": ["gamut", "gamma", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
"patterns": {"gamut": "rgb", "gamma": "gray", "cct": "gray", "contrast": "rgb", "accuracy": "accuracy"},
},
"hdr_movie": {
"name": "HDR Movie测试",
"test_items": ["gamut", "eotf", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
"patterns": {"gamut": "rgb", "eotf": "gray", "cct": "gray", "contrast": "rgb", "accuracy": "accuracy"},
},
}
_PATTERN_RGB = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[255, 0, 0], # 红色
[0, 255, 0], # 绿色
[0, 0, 255], # 蓝色
],
}
# 11点灰阶硬编码兜底文件缺失时使用主要数据来自 settings/patterns/gray.json
_PATTERN_GRAY_FALLBACK = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 10,
"pattern_params": [
[255, 255, 255], # 100% 白色
[230, 230, 230], # 90%
[205, 205, 205], # 80%
[179, 179, 179], # 70%
[154, 154, 154], # 60%
[128, 128, 128], # 50%
[102, 102, 102], # 40%
[78, 78, 78], # 30%
[52, 52, 52], # 20%
[26, 26, 26], # 10%
[0, 0, 0], # 0% 黑色
],
}
# 灰阶 pattern 从文件加载,支持用户编辑;文件缺失时回退到硬编码兜底
_PATTERN_GRAY = _load_pattern_or_empty(_PATTERNS_DIR / "gray.json", default=_PATTERN_GRAY_FALLBACK)
_PATTERN_ACCURACY = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 28, # 29个颜色最大索引是28
"pattern_params": [
# ========== 灰阶 (5个) ==========
[255, 255, 255], # 0: White
[230, 230, 230], # 1: Gray 80
[209, 209, 209], # 2: Gray 65
[186, 186, 186], # 3: Gray 50
[158, 158, 158], # 4: Gray 35
# ========== ColorChecker 24色 (18个) ==========
[115, 82, 66], # 5: Dark Skin
[194, 150, 130], # 6: Light Skin
[94, 122, 156], # 7: Blue Sky
[89, 107, 66], # 8: Foliage
[130, 128, 176], # 9: Blue Flower
[99, 189, 168], # 10: Bluish Green
[217, 120, 41], # 11: Orange
[74, 92, 163], # 12: Purplish Blue
[194, 84, 97], # 13: Moderate Red
[92, 61, 107], # 14: Purple
[158, 186, 64], # 15: Yellow Green
[230, 161, 46], # 16: Orange Yellow
[51, 61, 150], # 17: Blue (Legacy)
[71, 148, 71], # 18: Green (Legacy)
[176, 48, 59], # 19: Red (Legacy)
[237, 199, 33], # 20: Yellow (Legacy)
[186, 84, 145], # 21: Magenta (Legacy)
[0, 133, 163], # 22: Cyan (Legacy)
# ========== 100% 饱和色 (6个) ==========
[255, 0, 0], # 23: 100% Red
[0, 255, 0], # 24: 100% Green
[0, 0, 255], # 25: 100% Blue
[0, 255, 255], # 26: 100% Cyan
[255, 0, 255], # 27: 100% Magenta
[255, 255, 0], # 28: 100% Yellow
],
}
# _PATTERN_TEMP 从文件读取,可通过编辑 settings/patterns/temp.json 自定义
_PATTERN_TEMP = _load_pattern_or_empty(_PATTERNS_DIR / "temp.json")
# =============================================================================
# Pattern 注册表 —— 核心具名 pattern启动时解析
# =============================================================================
# 内置常量 pattern行业标准不依赖文件
_BUILTIN_PATTERNS = {
"rgb": _PATTERN_RGB,
"accuracy": _PATTERN_ACCURACY,
}
# 全部已知 pattern启动时加载用于 get_pattern 快速查找)
_KNOWN_PATTERNS = {
**_BUILTIN_PATTERNS,
"gray": _PATTERN_GRAY, # 从 gray.json 加载,有硬编码兜底
"custom": _PATTERN_TEMP, # 从 temp.json 加载(客户模板测试)
}
def get_pattern(name: str) -> dict:
"""
按名称返回 pattern 的深拷贝。
查找顺序:
1. ``_KNOWN_PATTERNS``(核心四类,启动时解析)
2. ``settings/patterns/{name}.json``(动态文件,如 pantone、客户定制等
文件不存在时返回空 pattern``pattern_params`` 为空列表)。
"""
if name in _KNOWN_PATTERNS:
return copy.deepcopy(_KNOWN_PATTERNS[name])
return _load_pattern_or_empty(_PATTERNS_DIR / f"{name}.json")
class PQConfig:
def __init__(self, current_test_type="screen_module"):
# ---- 向后兼容:只读属性,指向模块级常量 ----
self.default_cct_params_by_type = _DEFAULT_CCT_PARAMS
self.default_gamut_reference_by_type = _DEFAULT_GAMUT_REFERENCE
self.default_test_types = _DEFAULT_TEST_TYPES
self.default_pattern_rgb = _PATTERN_RGB
self.default_pattern_gray = _PATTERN_GRAY
self.default_pattern_accuracy = _PATTERN_ACCURACY
self.default_pattern_temp = _PATTERN_TEMP
# ---- 设备连接配置 ----
self.device_config = {
"ca_com": "COM1",
"ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0",
}
# ---- 自定义图案(用户可变)----
self.custom_pattern = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 0,
"pattern_params": [],
}
# ---- 运行态 ----
self.current_test_types = copy.deepcopy(_DEFAULT_TEST_TYPES)
self.current_test_type = current_test_type
self.current_pattern = copy.deepcopy(_PATTERN_RGB) # 深拷贝,避免引用污染
def get_default_cct_params(self, test_type):
"""获取指定测试类型的默认 CCT 参数副本。"""
default = self.default_cct_params_by_type.get(
test_type, self.default_cct_params_by_type["screen_module"]
)
return copy.deepcopy(default)
def get_default_gamut_reference(self, test_type):
"""获取指定测试类型的默认色域参考。"""
return self.default_gamut_reference_by_type.get(test_type, "DCI-P3")
# ========== 获取临时配置(用于 Full/Limited 转换)==========
def get_temp_config_with_converted_params(self, mode, converted_params):
"""
创建一个临时配置对象,包含转换后的 pattern 参数
Args:
mode: "rgb" | "gray" | "accuracy"
converted_params: 转换后的参数列表Full 或 Limited Range
Returns:
PQConfig: 临时配置对象(深拷贝,不影响原始配置)
"""
# 1. 深拷贝整个配置对象
temp_config = copy.deepcopy(self)
# 2. 设置正确的 pattern 模式
_resolved = get_pattern(mode)
temp_config.current_pattern = _resolved if _resolved["pattern_params"] else copy.deepcopy(_PATTERN_RGB)
# 3. 替换为转换后的参数
temp_config.current_pattern["pattern_params"] = converted_params
return temp_config
def to_dict(self):
"""将配置转换为字典格式"""
return {
"current_test_type": self.current_test_type,
"test_types": self.current_test_types,
"device_config": self.device_config,
"custom_pattern": self.custom_pattern,
}
def from_dict(self, config_dict):
"""从字典加载配置"""
self.current_test_type = config_dict.get("current_test_type", "screen_module")
self.current_test_types = config_dict.get("test_types", self.current_test_types)
self.device_config = config_dict.get("device_config", self.device_config)
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
def save_to_file(self, filename):
"""将配置保存到文件"""
with open(filename, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=4, ensure_ascii=False)
def set_current_test_type(self, test_type):
"""设置当前测试类型"""
if test_type in self.current_test_types:
self.current_test_type = test_type
return True
return False
def set_current_test_items(self, test_items):
"""设置当前测试类型的测试项"""
if self.current_test_type in self.current_test_types:
self.current_test_types[self.current_test_type]["test_items"] = test_items
return True
return False
def set_current_timing(self, timing):
if self.current_test_type in self.current_test_types:
self.current_test_types[self.current_test_type]["timing"] = timing
return True
return False
def set_device_config(self, ca_com, ucd_list, ca_channel):
"""设置设备连接配置"""
self.device_config["ca_com"] = ca_com
self.device_config["ucd_list"] = ucd_list
self.device_config["ca_channel"] = ca_channel
return True
def set_current_pattern(self, mode):
"""设置当前模式的测试图案(支持所有已注册名称及动态文件 pattern"""
pattern = get_pattern(mode)
if not pattern["pattern_params"]:
return False
self.current_pattern = pattern # get_pattern 已深拷贝
return True
def get_test_item_pattern(self, test_item, test_type=None) -> str:
"""
返回指定测试项目所使用的 pattern 名称。
从当前测试类型的 ``patterns`` 字段查找;未配置时按通用规则回退。
"""
tt = test_type or self.current_test_type
patterns = self.current_test_types.get(tt, {}).get("patterns", {})
_fallback = {
"gamut": "rgb",
"gamma": "gray",
"eotf": "gray",
"cct": "gray",
"contrast": "rgb",
"accuracy": "accuracy",
"custom": "custom",
}
return patterns.get(test_item) or _fallback.get(test_item, "gray")
def set_custom_pattern(self, pattern_mode, pattern_params):
"""设置自定义模式的测试项"""
self.custom_pattern["pattern_mode"] = pattern_mode
self.custom_pattern["pattern_params"] = pattern_params
self.custom_pattern["measurement_max_value"] = len(pattern_params) - 1
return True
# ========== 获取 29色名称列表 ==========
def get_accuracy_color_names(self):
"""
获取色准测试的 29个颜色名称SDR 和 HDR 通用)
Returns:
list: 29个颜色名称
"""
return [
# 灰阶 (5个)
"White",
"Gray 80",
"Gray 65",
"Gray 50",
"Gray 35",
# ColorChecker 24色 (18个)
"Dark Skin",
"Light Skin",
"Blue Sky",
"Foliage",
"Blue Flower",
"Bluish Green",
"Orange",
"Purplish Blue",
"Moderate Red",
"Purple",
"Yellow Green",
"Orange Yellow",
"Blue (Legacy)",
"Green (Legacy)",
"Red (Legacy)",
"Yellow (Legacy)",
"Magenta (Legacy)",
"Cyan (Legacy)",
# 100% 饱和色 (6个)
"100% Red",
"100% Green",
"100% Blue",
"100% Cyan",
"100% Magenta",
"100% Yellow",
]
# ========== 获取 29色的 RGB 值 ==========
def get_accuracy_color_rgb(self):
"""
获取色准测试的 RGB 值(用于标准值计算)
Returns:
list: [(name, r, g, b), ...]
"""
names = self.get_accuracy_color_names()
rgb_values = _PATTERN_ACCURACY["pattern_params"]
return [(name, r, g, b) for name, (r, g, b) in zip(names, rgb_values)]
def get_temp_pattern_names(self):
"""获取客户模板测试default_pattern_temp的固定 pattern 名称列表"""
percentages = list(range(100, -1, -5))
color_prefixes = ["W", "R", "G", "B", "Y", "C", "M"]
names = []
for prefix in color_prefixes:
for value in percentages:
names.append(f"{prefix} {value}%")
pattern_count = len(_PATTERN_TEMP.get("pattern_params", []))
if pattern_count <= len(names):
return names[:pattern_count]
# 兜底:如果后续扩展了 pattern 数量,补充通用名称,避免索引越界。
for i in range(len(names), pattern_count):
names.append(f"P {i + 1}")
return names
def get_test_item_chinese_names(self, test_items):
"""获取测试项目的显示名称"""
_name_map = {
"gamut": "色域",
"gamma": "Gamma",
"eotf": "EOTF",
"cct": "色度一致性",
"contrast": "对比度",
"accuracy": "色准",
}
return [_name_map.get(item, item) for item in test_items]
def get_current_config(self):
"""返回当前测试类型相关的所有配置信息"""
if self.current_test_type not in self.current_test_types:
return {}
current_test = self.current_test_types[self.current_test_type]
config_info = {
"test_type": self.current_test_type,
"test_name": current_test.get("name", "未知测试"),
"test_items": current_test.get("test_items", []),
"test_items_chinese": self.get_test_item_chinese_names(
current_test.get("test_items", [])
),
"timing": current_test.get("timing", "DMT 1920x 1080 @ 60Hz"),
"color_format": current_test.get("color_format", "RGB"),
"bpc": current_test.get("bpc", 8),
"colorimetry": current_test.get("colorimetry", "sRGB"),
}
return config_info