重构pq_config、修改切换面板清空旧逻辑

This commit is contained in:
xinzhu.yin
2026-05-19 11:50:53 +08:00
parent 405b6047b9
commit c42287b7d7
6 changed files with 324 additions and 687 deletions

View File

@@ -1,305 +1,246 @@
# PQ自动化测试配置模块 # PQ自动化测试配置模块
import json import json
import copy 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: class PQConfig:
def __init__(self, current_test_type="screen_module", device_config={}, pattern={}): def __init__(self, current_test_type="screen_module"):
self.default_cct_params_by_type = { # ---- 向后兼容:只读属性,指向模块级常量 ----
"screen_module": { self.default_cct_params_by_type = _DEFAULT_CCT_PARAMS
"x_ideal": 0.3127, self.default_gamut_reference_by_type = _DEFAULT_GAMUT_REFERENCE
"x_tolerance": 0.003, self.default_test_types = _DEFAULT_TEST_TYPES
"y_ideal": 0.3290, self.default_pattern_rgb = _PATTERN_RGB
"y_tolerance": 0.003, self.default_pattern_gray = _PATTERN_GRAY
}, self.default_pattern_accuracy = _PATTERN_ACCURACY
"sdr_movie": { self.default_pattern_temp = _PATTERN_TEMP
"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,
},
}
self.default_gamut_reference_by_type = {
"screen_module": "DCI-P3",
"sdr_movie": "BT.709",
"hdr_movie": "BT.2020",
}
self.default_test_types = { # ---- 设备连接配置 ----
"screen_module": {
"name": "屏模组性能测试",
"test_items": ["gamut", "gamma", "cct", "contrast"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
"sdr_movie": {
"name": "SDR Movie测试",
"test_items": ["gamut", "gamma", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
"hdr_movie": {
"name": "HDR Movie测试",
"test_items": ["gamut", "eotf", "cct", "contrast", "accuracy"],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
},
}
# 设备连接配置
self.device_config = { self.device_config = {
"ca_com": "COM1", "ca_com": "COM1",
"ucd_list": "0: UCD-323 [2128C209]", "ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0", "ca_channel": "0",
} }
# ========== RGB Pattern 配置 ========== # ---- 自定义图案(用户可变)----
self.default_pattern_rgb = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[255, 0, 0], # 红色
[0, 255, 0], # 绿色
[0, 0, 255], # 蓝色
],
}
# ========== 灰阶 Pattern 配置 ==========
self.default_pattern_gray = {
"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 配置29色 - SDR 和 HDR 通用)==========
self.default_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
],
}
self.default_pattern_temp = {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 146,
"pattern_params": [
[255, 255, 255],
[242, 242, 242],
[230, 230, 230],
[217, 217, 217],
[204, 204, 204],
[191, 191, 191],
[179, 179, 179],
[166, 166, 166],
[153, 153, 153],
[140, 140, 140],
[128, 128, 128],
[115, 115, 115],
[102, 102, 102],
[89, 89, 89],
[77, 77, 77],
[64, 64, 64],
[51, 51, 51],
[38, 38, 38],
[26, 26, 26],
[13, 13, 13],
[0, 0, 0],
[255, 0, 0],
[242, 0, 0],
[230, 0, 0],
[217, 0, 0],
[204, 0, 0],
[191, 0, 0],
[179, 0, 0],
[166, 0, 0],
[153, 0, 0],
[140, 0, 0],
[128, 0, 0],
[115, 0, 0],
[102, 0, 0],
[89, 0, 0],
[77, 0, 0],
[64, 0, 0],
[51, 0, 0],
[38, 0, 0],
[26, 0, 0],
[13, 0, 0],
[0, 0, 0],
[0, 255, 0],
[0, 242, 0],
[0, 230, 0],
[0, 217, 0],
[0, 204, 0],
[0, 191, 0],
[0, 179, 0],
[0, 166, 0],
[0, 153, 0],
[0, 140, 0],
[0, 128, 0],
[0, 115, 0],
[0, 102, 0],
[0, 89, 0],
[0, 77, 0],
[0, 64, 0],
[0, 51, 0],
[0, 38, 0],
[0, 26, 0],
[0, 13, 0],
[0, 0, 0],
[0, 0, 255],
[0, 0, 242],
[0, 0, 230],
[0, 0, 217],
[0, 0, 204],
[0, 0, 191],
[0, 0, 179],
[0, 0, 166],
[0, 0, 153],
[0, 0, 140],
[0, 0, 128],
[0, 0, 115],
[0, 0, 102],
[0, 0, 89],
[0, 0, 77],
[0, 0, 64],
[0, 0, 51],
[0, 0, 38],
[0, 0, 26],
[0, 0, 13],
[0, 0, 0],
[255, 255, 0],
[242, 242, 0],
[230, 230, 0],
[217, 217, 0],
[204, 204, 0],
[191, 191, 0],
[179, 179, 0],
[166, 166, 0],
[153, 153, 0],
[140, 140, 0],
[128, 128, 0],
[115, 115, 0],
[102, 102, 0],
[89, 89, 0],
[77, 77, 0],
[64, 64, 0],
[51, 51, 0],
[38, 38, 0],
[26, 26, 0],
[13, 13, 0],
[0, 0, 0],
[0, 255, 255],
[0, 242, 242],
[0, 230, 230],
[0, 217, 217],
[0, 204, 204],
[0, 191, 191],
[0, 179, 179],
[0, 166, 166],
[0, 153, 153],
[0, 140, 140],
[0, 128, 128],
[0, 115, 115],
[0, 102, 102],
[0, 89, 89],
[0, 77, 77],
[0, 64, 64],
[0, 51, 51],
[0, 38, 38],
[0, 26, 26],
[0, 13, 13],
[0, 0, 0],
[255, 0, 255],
[242, 0, 242],
[230, 0, 230],
[217, 0, 217],
[204, 0, 204],
[191, 0, 191],
[179, 0, 179],
[166, 0, 166],
[153, 0, 153],
[140, 0, 140],
[128, 0, 128],
[115, 0, 115],
[102, 0, 102],
[89, 0, 89],
[77, 0, 77],
[64, 0, 64],
[51, 0, 51],
[38, 0, 38],
[26, 0, 26],
[13, 0, 13],
[0, 0, 0],
]
}
# 自定义图案
self.custom_pattern = { self.custom_pattern = {
"pattern_mode": "SolidColor", "pattern_mode": "SolidColor",
"measurement_bit_depth": 8, "measurement_bit_depth": 8,
@@ -307,9 +248,10 @@ class PQConfig:
"pattern_params": [], "pattern_params": [],
} }
self.current_test_types = self.default_test_types # ---- 运行态 ----
self.current_test_types = copy.deepcopy(_DEFAULT_TEST_TYPES)
self.current_test_type = current_test_type self.current_test_type = current_test_type
self.current_pattern = self.default_pattern_rgb self.current_pattern = copy.deepcopy(_PATTERN_RGB) # 深拷贝,避免引用污染
def get_default_cct_params(self, test_type): def get_default_cct_params(self, test_type):
"""获取指定测试类型的默认 CCT 参数副本。""" """获取指定测试类型的默认 CCT 参数副本。"""
@@ -338,14 +280,8 @@ class PQConfig:
temp_config = copy.deepcopy(self) temp_config = copy.deepcopy(self)
# 2. 设置正确的 pattern 模式 # 2. 设置正确的 pattern 模式
if mode == "rgb": _resolved = get_pattern(mode)
temp_config.current_pattern = copy.deepcopy(self.default_pattern_rgb) temp_config.current_pattern = _resolved if _resolved["pattern_params"] else copy.deepcopy(_PATTERN_RGB)
elif mode == "gray":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_gray)
elif mode == "accuracy":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_accuracy)
elif mode == "custom":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_temp)
# 3. 替换为转换后的参数 # 3. 替换为转换后的参数
temp_config.current_pattern["pattern_params"] = converted_params temp_config.current_pattern["pattern_params"] = converted_params
@@ -358,9 +294,6 @@ class PQConfig:
"current_test_type": self.current_test_type, "current_test_type": self.current_test_type,
"test_types": self.current_test_types, "test_types": self.current_test_types,
"device_config": self.device_config, "device_config": self.device_config,
"default_pattern_rgb": self.default_pattern_rgb,
"default_pattern_gray": self.default_pattern_gray,
"default_pattern_accuracy": self.default_pattern_accuracy,
"custom_pattern": self.custom_pattern, "custom_pattern": self.custom_pattern,
} }
@@ -369,30 +302,6 @@ class PQConfig:
self.current_test_type = config_dict.get("current_test_type", "screen_module") 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.current_test_types = config_dict.get("test_types", self.current_test_types)
self.device_config = config_dict.get("device_config", self.device_config) self.device_config = config_dict.get("device_config", self.device_config)
self.default_pattern_rgb = config_dict.get(
"default_pattern_rgb", self.default_pattern_rgb
)
self.default_pattern_gray = config_dict.get(
"default_pattern_gray", self.default_pattern_gray
)
# ========== 强制使用新的 29色配置 ==========
loaded_accuracy = config_dict.get("default_pattern_accuracy", None)
# 检查加载的配置是否是旧的 10色
if loaded_accuracy and len(loaded_accuracy.get("pattern_params", [])) != 29:
print(
f"检测到旧的配置({len(loaded_accuracy.get('pattern_params', []))}色),强制使用新的 29色配置"
)
# 使用 __init__ 中定义的新配置
self.default_pattern_accuracy = self.default_pattern_accuracy
else:
self.default_pattern_accuracy = config_dict.get(
"default_pattern_accuracy", self.default_pattern_accuracy
)
# ==========================================
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern) self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
def save_to_file(self, filename): def save_to_file(self, filename):
@@ -428,32 +337,32 @@ class PQConfig:
return True return True
def set_current_pattern(self, mode): def set_current_pattern(self, mode):
"""设置当前模式的测试图案""" """设置当前模式的测试图案(支持所有已注册名称及动态文件 pattern"""
if mode == "rgb": pattern = get_pattern(mode)
self.current_pattern = self.default_pattern_rgb if not pattern["pattern_params"]:
elif mode == "gray":
self.current_pattern = self.default_pattern_gray
elif mode == "accuracy": # 色准模式SDR 和 HDR 通用 29色
self.current_pattern = self.default_pattern_accuracy
elif mode == "custom":
# self.current_pattern = self.custom_pattern
self.current_pattern = self.default_pattern_temp
else:
return False return False
self.current_pattern = pattern # get_pattern 已深拷贝
# 确保 measurement_max_value 是整数
if "measurement_max_value" in self.current_pattern:
value = self.current_pattern["measurement_max_value"]
if isinstance(value, str):
try:
self.current_pattern["measurement_max_value"] = int(value)
except ValueError:
self.current_pattern["measurement_max_value"] = (
len(self.current_pattern["pattern_params"]) - 1
)
return True 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): def set_custom_pattern(self, pattern_mode, pattern_params):
"""设置自定义模式的测试项""" """设置自定义模式的测试项"""
self.custom_pattern["pattern_mode"] = pattern_mode self.custom_pattern["pattern_mode"] = pattern_mode
@@ -513,7 +422,7 @@ class PQConfig:
list: [(name, r, g, b), ...] list: [(name, r, g, b), ...]
""" """
names = self.get_accuracy_color_names() names = self.get_accuracy_color_names()
rgb_values = self.default_pattern_accuracy["pattern_params"] rgb_values = _PATTERN_ACCURACY["pattern_params"]
return [(name, r, g, b) for name, (r, g, b) in zip(names, rgb_values)] return [(name, r, g, b) for name, (r, g, b) in zip(names, rgb_values)]
@@ -527,7 +436,7 @@ class PQConfig:
for value in percentages: for value in percentages:
names.append(f"{prefix} {value}%") names.append(f"{prefix} {value}%")
pattern_count = len(self.default_pattern_temp.get("pattern_params", [])) pattern_count = len(_PATTERN_TEMP.get("pattern_params", []))
if pattern_count <= len(names): if pattern_count <= len(names):
return names[:pattern_count] return names[:pattern_count]
@@ -540,23 +449,15 @@ class PQConfig:
def get_test_item_chinese_names(self, test_items): def get_test_item_chinese_names(self, test_items):
"""获取测试项目的显示名称""" """获取测试项目的显示名称"""
item_names = [] _name_map = {
for item in test_items: "gamut": "色域",
if item == "gamut": "gamma": "Gamma",
item_names.append("色域") "eotf": "EOTF",
elif item == "gamma": "cct": "色度一致性",
item_names.append("Gamma") "contrast": "对比度",
elif item == "eotf": "accuracy": "色准",
item_names.append("EOTF") }
elif item == "cct": return [_name_map.get(item, item) for item in test_items]
item_names.append("色度一致性")
elif item == "contrast":
item_names.append("对比度")
elif item == "accuracy":
item_names.append("色准")
else:
item_names.append(item)
return item_names
def get_current_config(self): def get_current_config(self):
"""返回当前测试类型相关的所有配置信息""" """返回当前测试类型相关的所有配置信息"""
@@ -579,62 +480,3 @@ class PQConfig:
} }
return config_info return config_info
# ========== 验证代码(测试完成后可删除)==========
if __name__ == "__main__":
print("=" * 60)
print("验证 pq_config.py 配置")
print("=" * 60)
config = PQConfig()
# 检查 default_pattern_accuracy
pattern_count = len(config.default_pattern_accuracy["pattern_params"])
max_value = config.default_pattern_accuracy["measurement_max_value"]
print(f"\ndefault_pattern_accuracy:")
print(f" 图案数量: {pattern_count}")
print(f" measurement_max_value: {max_value}")
if pattern_count == 29 and max_value == 28:
print("\n配置正确29色")
# 显示前 5 个图案
print("\n前5个图案:")
for i in range(5):
rgb = config.default_pattern_accuracy["pattern_params"][i]
names = config.get_accuracy_color_names()
print(f" [{i}] {names[i]:15s} RGB{rgb}")
# 显示后 5 个图案
print("\n后5个图案:")
for i in range(24, 29):
rgb = config.default_pattern_accuracy["pattern_params"][i]
names = config.get_accuracy_color_names()
print(f" [{i}] {names[i]:15s} RGB{rgb}")
# 测试 set_current_pattern
print("\n测试 set_current_pattern('accuracy'):")
config.set_current_pattern("accuracy")
current_count = len(config.current_pattern["pattern_params"])
print(f" current_pattern 图案数量: {current_count}")
if current_count == 29:
print(" set_current_pattern 工作正常")
else:
print(f" set_current_pattern 失败!只有 {current_count} 个图案")
else:
print(f"\n配置错误!")
print(f" 期望: 29 个图案, measurement_max_value=28")
print(f" 实际: {pattern_count} 个图案, measurement_max_value={max_value}")
print("\n请检查 default_pattern_accuracy 定义!")
print(" 应该包含:")
print(" - 5个灰阶")
print(" - 18个 ColorChecker 色块")
print(" - 6个 100% 饱和色")
print(" - 总计 29 个 RGB 数组")
print("=" * 60)

View File

@@ -583,6 +583,7 @@ def test_gamut(self, test_type):
# 传递完整的 results 用于绘图 # 传递完整的 results 用于绘图
self.plot_gamut(results, coverage, test_type) self.plot_gamut(results, coverage, test_type)
self._save_chart_snapshot(test_type, "gamut", (results, coverage, test_type))
self.log_gui.log("色域测试完成", level="success") self.log_gui.log("色域测试完成", level="success")
@@ -674,6 +675,7 @@ def test_gamma(self, test_type, gray_data=None):
target_gamma = 2.2 target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type) self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type))
self.log_gui.log("Gamma测试完成", level="success") self.log_gui.log("Gamma测试完成", level="success")
@@ -756,6 +758,7 @@ def test_eotf(self, test_type, gray_data=None):
# ========== 绘制 EOTF 曲线 ========== # ========== 绘制 EOTF 曲线 ==========
# HDR 使用 PQ 曲线,目标 gamma 设为 None不使用传统 gamma # HDR 使用 PQ 曲线,目标 gamma 设为 None不使用传统 gamma
self.plot_eotf(L_bar, results_with_eotf_list, test_type) self.plot_eotf(L_bar, results_with_eotf_list, test_type)
self._save_chart_snapshot(test_type, "eotf", (L_bar, results_with_eotf_list, test_type))
self.log_gui.log("EOTF 测试完成", level="success") self.log_gui.log("EOTF 测试完成", level="success")
@@ -796,6 +799,7 @@ def test_cct(self, test_type, gray_data=None):
# 绘制图表 # 绘制图表
self.plot_cct(test_type) self.plot_cct(test_type)
self._save_chart_snapshot(test_type, "cct", (test_type,))
self.log_gui.log("色度一致性测试完成", level="success") self.log_gui.log("色度一致性测试完成", level="success")
except Exception as e: except Exception as e:
@@ -859,6 +863,7 @@ def test_contrast(self, test_type, gray_data=None):
# 绘制对比度图表 # 绘制对比度图表
self.plot_contrast(contrast_data, test_type) self.plot_contrast(contrast_data, test_type)
self._save_chart_snapshot(test_type, "contrast", (contrast_data, test_type))
self.log_gui.log("对比度测试完成", level="success") self.log_gui.log("对比度测试完成", level="success")
except Exception as e: except Exception as e:
@@ -1024,6 +1029,7 @@ def test_color_accuracy(self, test_type):
# ========== 绘制图表 ========== # ========== 绘制图表 ==========
self.plot_accuracy(accuracy_data, test_type) self.plot_accuracy(accuracy_data, test_type)
self._save_chart_snapshot(test_type, "accuracy", (accuracy_data, test_type))
self.log_gui.log("色准测试完成", level="success") self.log_gui.log("色准测试完成", level="success")

View File

@@ -4,6 +4,7 @@ import copy
from dataclasses import dataclass from dataclasses import dataclass
from app.data_range_converter import convert_pattern_params from app.data_range_converter import convert_pattern_params
from app.pq.pq_config import get_pattern
from drivers.ucd_helpers import send_solid_rgb_pattern from drivers.ucd_helpers import send_solid_rgb_pattern
@@ -166,16 +167,7 @@ class PatternService:
return active_config return active_config
def _get_source_pattern_params(self, mode): def _get_source_pattern_params(self, mode):
config = self.app.config return copy.deepcopy(get_pattern(mode)["pattern_params"])
if mode == "rgb":
return copy.deepcopy(config.default_pattern_rgb["pattern_params"])
if mode == "gray":
return copy.deepcopy(config.default_pattern_gray["pattern_params"])
if mode == "accuracy":
return copy.deepcopy(config.default_pattern_accuracy["pattern_params"])
if mode == "custom":
return copy.deepcopy(config.default_pattern_temp["pattern_params"])
raise ValueError(f"未知的图案模式: {mode}")
def _get_display_names(self, mode, total_patterns): def _get_display_names(self, mode, total_patterns):
if mode == "accuracy": if mode == "accuracy":

View File

@@ -659,9 +659,12 @@ def clear_chart(self):
self.eotf_canvas.draw() self.eotf_canvas.draw()
# ========== 4. 清空色度图表 ========== # ========== 4. 清空色度图表 ==========
if hasattr(self, "cct_ax1") and hasattr(self, "cct_ax2"): # 注意plot_cct 会调用 cct_fig.clear() 并重建 subplots导致 self.cct_ax1/ax2 变成
# 上图x coordinates # 过期引用。因此清空时必须同样重建,并更新引用,否则清不干净。
self.cct_ax1.clear() if hasattr(self, "cct_fig") and hasattr(self, "cct_canvas"):
self.cct_fig.clear()
self.cct_ax1 = self.cct_fig.add_subplot(211)
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9) self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
self.cct_ax1.set_ylabel("CIE x", fontsize=9) self.cct_ax1.set_ylabel("CIE x", fontsize=9)
self.cct_ax1.set_xlim(0, 105) self.cct_ax1.set_xlim(0, 105)
@@ -669,8 +672,7 @@ def clear_chart(self):
self.cct_ax1.grid(True, linestyle="--", alpha=0.3) self.cct_ax1.grid(True, linestyle="--", alpha=0.3)
self.cct_ax1.tick_params(labelsize=8) self.cct_ax1.tick_params(labelsize=8)
# 下图y coordinates self.cct_ax2 = self.cct_fig.add_subplot(212)
self.cct_ax2.clear()
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9) self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
self.cct_ax2.set_ylabel("CIE y", fontsize=9) self.cct_ax2.set_ylabel("CIE y", fontsize=9)
self.cct_ax2.set_xlim(0, 105) self.cct_ax2.set_xlim(0, 105)
@@ -679,8 +681,6 @@ def clear_chart(self):
self.cct_ax2.tick_params(labelsize=8) self.cct_ax2.tick_params(labelsize=8)
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985) self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
# 重置布局
self.cct_fig.subplots_adjust( self.cct_fig.subplots_adjust(
left=0.12, left=0.12,
right=0.88, right=0.88,
@@ -688,7 +688,6 @@ def clear_chart(self):
bottom=0.08, bottom=0.08,
hspace=0.25, hspace=0.25,
) )
self.cct_canvas.draw() self.cct_canvas.draw()
# ========== 5. 清空对比度图表 ========== # ========== 5. 清空对比度图表 ==========

View File

@@ -145,6 +145,8 @@ class PQAutomationApp:
self.pattern_service = PatternService(self) self.pattern_service = PatternService(self)
# 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误 # 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误
self.results = PQResultStore() self.results = PQResultStore()
# 图表快照:按 test_type 缓存最近一次各图表的绘制参数,切换类型时可恢复
self._chart_snapshots: dict = {} # {test_type: {chart_name: args_tuple}}
# 加载上次保存的设置 # 加载上次保存的设置
self.config_file = self.get_config_path() self.config_file = self.get_config_path()
@@ -541,6 +543,34 @@ class PQAutomationApp:
self._switch_signal_format_tabs(test_type) self._switch_signal_format_tabs(test_type)
self._switch_chart_tabs_by_test_type(test_type) self._switch_chart_tabs_by_test_type(test_type)
self.sync_gamut_toolbar() self.sync_gamut_toolbar()
self._restore_charts_for_type(test_type)
def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
"""保存某次绘图的参数,以便切换测试类型时可以重绘。"""
if test_type not in self._chart_snapshots:
self._chart_snapshots[test_type] = {}
self._chart_snapshots[test_type][chart_name] = args
def _restore_charts_for_type(self, test_type: str):
"""
切换测试类型后恢复图表显示:
- 该类型有历史结果 → 切换活跃结果 + 重绘所有已缓存图表
- 该类型无历史结果 → 清空图表 + 禁用保存按钮
"""
self.results.set_active(test_type)
snapshots = self._chart_snapshots.get(test_type)
if not snapshots:
self.clear_chart()
if hasattr(self, "save_btn"):
self.save_btn.config(state=tk.DISABLED)
return
for chart_name, args in snapshots.items():
plot_fn = getattr(self, f"plot_{chart_name}", None)
if plot_fn:
try:
plot_fn(*args)
except Exception:
pass
def _check_start_preconditions(self): def _check_start_preconditions(self):
"""检查开始测试前置条件:设备连接 & 未在测试中。""" """检查开始测试前置条件:设备连接 & 未在测试中。"""

View File

@@ -1,5 +1,5 @@
{ {
"current_test_type": "screen_module", "current_test_type": "sdr_movie",
"test_types": { "test_types": {
"screen_module": { "screen_module": {
"name": "屏模组性能测试", "name": "屏模组性能测试",
@@ -19,12 +19,16 @@
"y_ideal": 0.329, "y_ideal": 0.329,
"y_tolerance": 0.003 "y_tolerance": 0.003
}, },
"gamut_reference": "BT.709" "gamut_reference": "DCI-P3"
}, },
"sdr_movie": { "sdr_movie": {
"name": "SDR Movie测试", "name": "SDR Movie测试",
"test_items": [ "test_items": [
"gamut" "gamut",
"gamma",
"cct",
"contrast",
"accuracy"
], ],
"timing": "DMT 1920x 1080 @ 60Hz", "timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB", "color_format": "RGB",
@@ -36,7 +40,7 @@
"y_ideal": 0.329, "y_ideal": 0.329,
"y_tolerance": 0.003 "y_tolerance": 0.003
}, },
"gamut_reference": "BT.2020" "gamut_reference": "DCI-P3"
}, },
"hdr_movie": { "hdr_movie": {
"name": "HDR Movie测试", "name": "HDR Movie测试",
@@ -60,246 +64,10 @@
} }
}, },
"device_config": { "device_config": {
"ca_com": "COM3", "ca_com": "COM6",
"ucd_list": "0: UCD-323 [2128C209]", "ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0" "ca_channel": "0"
}, },
"default_pattern_rgb": {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 2,
"pattern_params": [
[
255,
0,
0
],
[
0,
255,
0
],
[
0,
0,
255
]
]
},
"default_pattern_gray": {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 10,
"pattern_params": [
[
255,
255,
255
],
[
230,
230,
230
],
[
205,
205,
205
],
[
179,
179,
179
],
[
154,
154,
154
],
[
128,
128,
128
],
[
102,
102,
102
],
[
78,
78,
78
],
[
52,
52,
52
],
[
26,
26,
26
],
[
0,
0,
0
]
]
},
"default_pattern_accuracy": {
"pattern_mode": "SolidColor",
"measurement_bit_depth": 8,
"measurement_max_value": 28,
"pattern_params": [
[
255,
255,
255
],
[
230,
230,
230
],
[
209,
209,
209
],
[
186,
186,
186
],
[
158,
158,
158
],
[
115,
82,
66
],
[
194,
150,
130
],
[
94,
122,
156
],
[
89,
107,
66
],
[
130,
128,
176
],
[
99,
189,
168
],
[
217,
120,
41
],
[
74,
92,
163
],
[
194,
84,
97
],
[
92,
61,
107
],
[
158,
186,
64
],
[
230,
161,
46
],
[
51,
61,
150
],
[
71,
148,
71
],
[
176,
48,
59
],
[
237,
199,
33
],
[
186,
84,
145
],
[
0,
133,
163
],
[
255,
0,
0
],
[
0,
255,
0
],
[
0,
0,
255
],
[
0,
255,
255
],
[
255,
0,
255
],
[
255,
255,
0
]
]
},
"custom_pattern": { "custom_pattern": {
"pattern_mode": "SolidColor", "pattern_mode": "SolidColor",
"measurement_bit_depth": 8, "measurement_bit_depth": 8,