diff --git a/app/pq/pq_config.py b/app/pq/pq_config.py index c3f0cef..73b2dda 100644 --- a/app/pq/pq_config.py +++ b/app/pq/pq_config.py @@ -1,305 +1,246 @@ # 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", device_config={}, pattern={}): - self.default_cct_params_by_type = { - "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, - }, - } - self.default_gamut_reference_by_type = { - "screen_module": "DCI-P3", - "sdr_movie": "BT.709", - "hdr_movie": "BT.2020", - } + 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.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 = { "ca_com": "COM1", "ucd_list": "0: UCD-323 [2128C209]", "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 = { "pattern_mode": "SolidColor", "measurement_bit_depth": 8, @@ -307,9 +248,10 @@ class PQConfig: "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_pattern = self.default_pattern_rgb + self.current_pattern = copy.deepcopy(_PATTERN_RGB) # 深拷贝,避免引用污染 def get_default_cct_params(self, test_type): """获取指定测试类型的默认 CCT 参数副本。""" @@ -338,14 +280,8 @@ class PQConfig: temp_config = copy.deepcopy(self) # 2. 设置正确的 pattern 模式 - if mode == "rgb": - temp_config.current_pattern = copy.deepcopy(self.default_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) + _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 @@ -358,9 +294,6 @@ class PQConfig: "current_test_type": self.current_test_type, "test_types": self.current_test_types, "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, } @@ -369,30 +302,6 @@ class PQConfig: 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.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) def save_to_file(self, filename): @@ -428,32 +337,32 @@ class PQConfig: return True def set_current_pattern(self, mode): - """设置当前模式的测试图案""" - if mode == "rgb": - self.current_pattern = self.default_pattern_rgb - 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: + """设置当前模式的测试图案(支持所有已注册名称及动态文件 pattern)。""" + pattern = get_pattern(mode) + if not pattern["pattern_params"]: return False - - # 确保 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 - ) - + 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 @@ -513,7 +422,7 @@ class PQConfig: list: [(name, r, g, b), ...] """ 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)] @@ -527,7 +436,7 @@ class PQConfig: for value in percentages: 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): return names[:pattern_count] @@ -540,23 +449,15 @@ class PQConfig: def get_test_item_chinese_names(self, test_items): """获取测试项目的显示名称""" - item_names = [] - for item in test_items: - if item == "gamut": - item_names.append("色域") - elif item == "gamma": - item_names.append("Gamma") - elif item == "eotf": - item_names.append("EOTF") - elif item == "cct": - item_names.append("色度一致性") - elif item == "contrast": - item_names.append("对比度") - elif item == "accuracy": - item_names.append("色准") - else: - item_names.append(item) - return item_names + _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): """返回当前测试类型相关的所有配置信息""" @@ -578,63 +479,4 @@ class PQConfig: "colorimetry": current_test.get("colorimetry", "sRGB"), } - 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) + return config_info \ No newline at end of file diff --git a/app/runner/test_runner.py b/app/runner/test_runner.py index cf110b8..3b67fcf 100644 --- a/app/runner/test_runner.py +++ b/app/runner/test_runner.py @@ -583,6 +583,7 @@ def test_gamut(self, test_type): # 传递完整的 results 用于绘图 self.plot_gamut(results, coverage, test_type) + self._save_chart_snapshot(test_type, "gamut", (results, coverage, test_type)) self.log_gui.log("色域测试完成", level="success") @@ -674,6 +675,7 @@ def test_gamma(self, test_type, gray_data=None): target_gamma = 2.2 self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type) + self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type)) self.log_gui.log("Gamma测试完成", level="success") @@ -756,6 +758,7 @@ def test_eotf(self, test_type, gray_data=None): # ========== 绘制 EOTF 曲线 ========== # HDR 使用 PQ 曲线,目标 gamma 设为 None(不使用传统 gamma) 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") @@ -796,6 +799,7 @@ def test_cct(self, test_type, gray_data=None): # 绘制图表 self.plot_cct(test_type) + self._save_chart_snapshot(test_type, "cct", (test_type,)) self.log_gui.log("色度一致性测试完成", level="success") 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._save_chart_snapshot(test_type, "contrast", (contrast_data, test_type)) self.log_gui.log("对比度测试完成", level="success") except Exception as e: @@ -1024,6 +1029,7 @@ def test_color_accuracy(self, 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") diff --git a/app/services/pattern_service.py b/app/services/pattern_service.py index 5029a94..39274f3 100644 --- a/app/services/pattern_service.py +++ b/app/services/pattern_service.py @@ -4,6 +4,7 @@ import copy from dataclasses import dataclass 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 @@ -166,16 +167,7 @@ class PatternService: return active_config def _get_source_pattern_params(self, mode): - config = self.app.config - 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}") + return copy.deepcopy(get_pattern(mode)["pattern_params"]) def _get_display_names(self, mode, total_patterns): if mode == "accuracy": diff --git a/app/views/chart_frame.py b/app/views/chart_frame.py index 1faaf40..bd0b62d 100644 --- a/app/views/chart_frame.py +++ b/app/views/chart_frame.py @@ -659,9 +659,12 @@ def clear_chart(self): self.eotf_canvas.draw() # ========== 4. 清空色度图表 ========== - if hasattr(self, "cct_ax1") and hasattr(self, "cct_ax2"): - # 上图:x coordinates - self.cct_ax1.clear() + # 注意:plot_cct 会调用 cct_fig.clear() 并重建 subplots,导致 self.cct_ax1/ax2 变成 + # 过期引用。因此清空时必须同样重建,并更新引用,否则清不干净。 + 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_ylabel("CIE x", fontsize=9) 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.tick_params(labelsize=8) - # 下图:y coordinates - self.cct_ax2.clear() + self.cct_ax2 = self.cct_fig.add_subplot(212) self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9) self.cct_ax2.set_ylabel("CIE y", fontsize=9) self.cct_ax2.set_xlim(0, 105) @@ -679,8 +681,6 @@ def clear_chart(self): self.cct_ax2.tick_params(labelsize=8) self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985) - - # 重置布局 self.cct_fig.subplots_adjust( left=0.12, right=0.88, @@ -688,7 +688,6 @@ def clear_chart(self): bottom=0.08, hspace=0.25, ) - self.cct_canvas.draw() # ========== 5. 清空对比度图表 ========== diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 2119190..5eb1cac 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -145,6 +145,8 @@ class PQAutomationApp: self.pattern_service = PatternService(self) # 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误 self.results = PQResultStore() + # 图表快照:按 test_type 缓存最近一次各图表的绘制参数,切换类型时可恢复 + self._chart_snapshots: dict = {} # {test_type: {chart_name: args_tuple}} # 加载上次保存的设置 self.config_file = self.get_config_path() @@ -541,6 +543,34 @@ class PQAutomationApp: self._switch_signal_format_tabs(test_type) self._switch_chart_tabs_by_test_type(test_type) 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): """检查开始测试前置条件:设备连接 & 未在测试中。""" diff --git a/settings/pq_config.json b/settings/pq_config.json index 684ffb3..89e3c20 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "screen_module", + "current_test_type": "sdr_movie", "test_types": { "screen_module": { "name": "屏模组性能测试", @@ -19,12 +19,16 @@ "y_ideal": 0.329, "y_tolerance": 0.003 }, - "gamut_reference": "BT.709" + "gamut_reference": "DCI-P3" }, "sdr_movie": { "name": "SDR Movie测试", "test_items": [ - "gamut" + "gamut", + "gamma", + "cct", + "contrast", + "accuracy" ], "timing": "DMT 1920x 1080 @ 60Hz", "color_format": "RGB", @@ -36,7 +40,7 @@ "y_ideal": 0.329, "y_tolerance": 0.003 }, - "gamut_reference": "BT.2020" + "gamut_reference": "DCI-P3" }, "hdr_movie": { "name": "HDR Movie测试", @@ -60,246 +64,10 @@ } }, "device_config": { - "ca_com": "COM3", + "ca_com": "COM6", "ucd_list": "0: UCD-323 [2128C209]", "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": { "pattern_mode": "SolidColor", "measurement_bit_depth": 8,