# 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", "data_range": "Full", "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", "data_range": "Full", "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", "data_range": "Full", "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") def reload_gray_pattern() -> dict: """重新从 ``settings/patterns/gray.json`` 加载灰阶 pattern。 原地更新 ``_PATTERN_GRAY``,让 ``PQConfig.default_pattern_gray`` 与 ``_KNOWN_PATTERNS['gray']`` 等所有现有引用同步生效, 无需重启程序即可应用新 pattern 列表。 """ new_data = _load_pattern_or_empty( _PATTERNS_DIR / "gray.json", default=_PATTERN_GRAY_FALLBACK ) _PATTERN_GRAY.clear() _PATTERN_GRAY.update(new_data) return copy.deepcopy(_PATTERN_GRAY) def get_gray_pattern_fallback() -> dict: """返回硬编码默认 11 点灰阶 pattern 的深拷贝(用于 UI 的"恢复默认")。""" return copy.deepcopy(_PATTERN_GRAY_FALLBACK) # ============================================================================= # 灰阶 Pattern 预设管理(settings/patterns/presets/gray/*.json) # ============================================================================= # # 设计要点: # - 每个预设独立 JSON 文件,文件名(不含 .json)即预设名。 # - 内置预设以 ``_builtin_`` 前缀命名,并在 _meta.locked=True,UI 禁止删除/改名/覆盖。 # - 当前激活预设记录在 settings/patterns/presets/_active.json,便于 UI 显示。 # - 应用某预设 = 把它复制写入 settings/patterns/gray.json + reload_gray_pattern()。 # - gamma/cct/contrast/eotf 共用同一份 gray 预设(与 runner 现有共享灰阶采集对齐)。 # _PRESETS_DIR = _PATTERNS_DIR / "presets" _ACTIVE_INDEX_FILE = _PRESETS_DIR / "_active.json" def _gray_presets_dir() -> Path: p = _PRESETS_DIR / "gray" p.mkdir(parents=True, exist_ok=True) return p def _safe_preset_name(name: str) -> str: """清洗预设名,去掉文件系统危险字符。""" name = (name or "").strip() bad = '<>:"/\\|?*\n\r\t' for ch in bad: name = name.replace(ch, "_") return name[:80] or "untitled" def _preset_path(test_kind: str, name: str) -> Path: if test_kind != "gray": raise ValueError(f"暂仅支持 test_kind='gray',收到: {test_kind}") return _gray_presets_dir() / f"{_safe_preset_name(name)}.json" def _load_active_index() -> dict: try: with open(_ACTIVE_INDEX_FILE, encoding="utf-8") as f: data = json.load(f) return data if isinstance(data, dict) else {} except (FileNotFoundError, json.JSONDecodeError): return {} def _save_active_index(data: dict) -> None: _PRESETS_DIR.mkdir(parents=True, exist_ok=True) with open(_ACTIVE_INDEX_FILE, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) def list_presets(test_kind: str = "gray") -> list[dict]: """ 列出指定类别下的所有预设。 Returns: list of {name, locked, description, point_count, file_path, generator} """ d = _gray_presets_dir() if test_kind == "gray" else None if d is None: return [] items: list[dict] = [] for fp in sorted(d.glob("*.json")): try: data = load_pattern_file(fp) except (json.JSONDecodeError, OSError): continue meta = data.get("_meta", {}) or {} items.append({ "name": fp.stem, "locked": bool(meta.get("locked", False)), "description": meta.get("description", ""), "generator": meta.get("generator", ""), "created": meta.get("created", ""), "point_count": len(data.get("pattern_params") or []), "file_path": str(fp), }) # 内置 _builtin_ 排前,其余按名字排序 items.sort(key=lambda x: (not x["name"].startswith("_builtin_"), x["name"])) return items def load_preset(test_kind: str, name: str) -> dict: """加载预设的完整 pattern 数据(含 _meta)。""" return load_pattern_file(_preset_path(test_kind, name)) def save_preset( test_kind: str, name: str, pattern: dict, *, description: str = "", generator: str = "", locked: bool = False, overwrite: bool = True, ) -> Path: """保存预设。若 overwrite=False 且文件已存在或目标为锁定预设则抛错。""" from datetime import datetime name = _safe_preset_name(name) path = _preset_path(test_kind, name) if path.exists(): try: existing = load_pattern_file(path) if (existing.get("_meta") or {}).get("locked"): raise PermissionError(f"预设 '{name}' 已锁定,不可覆盖") except (json.JSONDecodeError, OSError): pass if not overwrite: raise FileExistsError(f"预设 '{name}' 已存在") data = { "pattern_mode": pattern.get("pattern_mode", "SolidColor"), "measurement_bit_depth": pattern.get("measurement_bit_depth", 8), "measurement_max_value": max(0, len(pattern.get("pattern_params") or []) - 1), "pattern_params": [list(map(int, rgb)) for rgb in (pattern.get("pattern_params") or [])], "_meta": { "description": description, "generator": generator, "locked": locked, "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), }, } save_pattern_file(path, data) return path def delete_preset(test_kind: str, name: str) -> None: """删除预设;锁定预设不可删除。""" path = _preset_path(test_kind, name) if not path.exists(): raise FileNotFoundError(f"预设 '{name}' 不存在") data = load_pattern_file(path) if (data.get("_meta") or {}).get("locked"): raise PermissionError(f"预设 '{name}' 已锁定,不可删除") path.unlink() # 若被删的恰是激活预设,清理记录 idx = _load_active_index() if idx.get(test_kind) == name: idx.pop(test_kind, None) _save_active_index(idx) def rename_preset(test_kind: str, old: str, new: str) -> Path: """重命名;锁定预设不可改名。""" src = _preset_path(test_kind, old) if not src.exists(): raise FileNotFoundError(f"预设 '{old}' 不存在") data = load_pattern_file(src) if (data.get("_meta") or {}).get("locked"): raise PermissionError(f"预设 '{old}' 已锁定,不可重命名") dst = _preset_path(test_kind, new) if dst.exists(): raise FileExistsError(f"目标预设 '{new}' 已存在") src.rename(dst) idx = _load_active_index() if idx.get(test_kind) == old: idx[test_kind] = dst.stem _save_active_index(idx) return dst def duplicate_preset(test_kind: str, src_name: str, new_name: str) -> Path: """复制一个预设为新副本(解除锁定)。""" data = load_preset(test_kind, src_name) meta = dict(data.get("_meta") or {}) meta["locked"] = False meta["description"] = f"复制自 {src_name}" + ( f";{meta.get('description', '')}" if meta.get("description") else "" ) data["_meta"] = meta return save_preset( test_kind, new_name, data, description=meta["description"], generator=meta.get("generator", ""), locked=False, overwrite=False, ) def activate_preset(test_kind: str, name: str) -> dict: """ 将指定预设应用为当前 gray pattern: - 写入 settings/patterns/gray.json(剥离 _meta 以保持原格式) - reload_gray_pattern() 让运行时立即生效 - 在 _active.json 记录激活预设名 """ data = load_preset(test_kind, name) clean = { "pattern_mode": data.get("pattern_mode", "SolidColor"), "measurement_bit_depth": data.get("measurement_bit_depth", 8), "measurement_max_value": data.get("measurement_max_value", 0), "pattern_params": data.get("pattern_params") or [], } save_pattern_file(_PATTERNS_DIR / "gray.json", clean) reload_gray_pattern() idx = _load_active_index() idx[test_kind] = _safe_preset_name(name) _save_active_index(idx) return clean def get_active_preset_name(test_kind: str = "gray") -> str | None: """返回当前激活预设名(来自 _active.json);不存在则返回 None。""" return _load_active_index().get(test_kind) def import_preset_from_file(test_kind: str, src_file, *, name: str | None = None) -> Path: """从外部 JSON 文件导入为预设。""" data = load_pattern_file(src_file) if not data.get("pattern_params"): raise ValueError("文件中未找到 pattern_params") preset_name = name or Path(src_file).stem return save_preset( test_kind, preset_name, data, description=(data.get("_meta") or {}).get("description", "导入"), generator=(data.get("_meta") or {}).get("generator", ""), locked=False, overwrite=True, ) def export_preset_to_file(test_kind: str, name: str, dst_file) -> Path: """将预设导出到指定外部 JSON 文件。""" data = load_preset(test_kind, name) save_pattern_file(dst_file, data) return Path(dst_file) # ---- 内置预设生成器 ---------------------------------------------------------- def _gen_even_gray(n: int) -> list[list[int]]: """N 点等分 (100%→0%)。""" if n < 2: n = 2 out = [] for i in range(n): pct = 100.0 - (100.0 / (n - 1)) * i v = int(round(pct / 100.0 * 255)) out.append([v, v, v]) return out def _gen_pq_gray(n: int) -> list[list[int]]: """在 PQ 编码空间 N 点等分(亮度按 PQ 曲线均匀分布,低端更密集)。 采样规则:取 PQ 信号值从 1.0 到 0.0 等分,转 8-bit RGB 灰阶。 (PQ 信号本身在感知亮度上即为线性,故"等分编码值"≈"等分感知亮度"。) """ if n < 2: n = 2 out = [] for i in range(n): v_pq = 1.0 - i / (n - 1) v = int(round(v_pq * 255)) out.append([v, v, v]) return out def _gen_gamma_gray(n: int, gamma: float = 2.2) -> list[list[int]]: """在线性光强空间 N 点等分,再用 gamma 编码到 8-bit 灰阶(暗端更密集)。""" if n < 2: n = 2 out = [] for i in range(n): lin = 1.0 - i / (n - 1) # 线性光 1→0 code = lin ** (1.0 / gamma) # gamma 编码 v = int(round(code * 255)) out.append([v, v, v]) return out _BUILTIN_GRAY_PRESETS = [ ("_builtin_even_11pt", "11 点等分 (100%→0%),行业标准灰阶", "even-11", _gen_even_gray(11)), ("_builtin_even_21pt", "21 点等分 (5% 步长),更精细的 SDR 灰阶", "even-21", _gen_even_gray(21)), ("_builtin_gamma22_17pt", "17 点 Gamma 2.2 分布(暗端更密集),用于 SDR Gamma 拟合", "gamma2.2-17", _gen_gamma_gray(17, 2.2)), ("_builtin_pq_17pt", "17 点 PQ 编码等分,用于 HDR EOTF 评估", "pq-17", _gen_pq_gray(17)), ] def ensure_builtin_presets() -> None: """启动时调用:确保内置预设存在;若已存在则跳过(不覆盖用户修改)。 同时迁移:若 presets/ 目录为空但 settings/patterns/gray.json 存在, 将其作为 ``user_current`` 预设引入,避免用户原有自定义丢失。 """ presets_dir = _gray_presets_dir() for name, desc, gen, params in _BUILTIN_GRAY_PRESETS: path = presets_dir / f"{name}.json" if path.exists(): continue save_preset( "gray", name, { "pattern_mode": "SolidColor", "measurement_bit_depth": 8, "measurement_max_value": len(params) - 1, "pattern_params": params, }, description=desc, generator=gen, locked=True, ) # 迁移:用户原有 gray.json 入库为 user_current(仅在 presets 目录还没有任何用户预设时) user_presets = [p for p in presets_dir.glob("*.json") if not p.stem.startswith("_builtin_")] if not user_presets: gray_file = _PATTERNS_DIR / "gray.json" if gray_file.exists(): try: cur = load_pattern_file(gray_file) if cur.get("pattern_params"): save_preset( "gray", "user_current", cur, description="迁移自原 gray.json", generator="migrated", locked=False, overwrite=False, ) idx = _load_active_index() idx.setdefault("gray", "user_current") _save_active_index(idx) except (json.JSONDecodeError, OSError, FileExistsError): pass # 若无激活预设记录,按规则推断(优先匹配当前 gray.json 内容) idx = _load_active_index() if "gray" not in idx: try: cur = load_pattern_file(_PATTERNS_DIR / "gray.json") cur_params = cur.get("pattern_params") or [] except (FileNotFoundError, json.JSONDecodeError, OSError): cur_params = [] match: str | None = None for fp in presets_dir.glob("*.json"): try: if (load_pattern_file(fp).get("pattern_params") or []) == cur_params: match = fp.stem break except (json.JSONDecodeError, OSError): continue if match: idx["gray"] = match _save_active_index(idx) # 自动确保内置预设存在(首次启动会创建文件) try: ensure_builtin_presets() except OSError: # 启动期目录不可写则跳过;UI 层有兜底 pass 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") # 以默认模板为底,叠加历史配置,保证新字段(如 data_range)在旧配置下也有值。 loaded_test_types = config_dict.get("test_types", {}) merged_test_types = copy.deepcopy(_DEFAULT_TEST_TYPES) if isinstance(loaded_test_types, dict): for test_type, loaded_cfg in loaded_test_types.items(): if test_type in merged_test_types and isinstance(loaded_cfg, dict): merged_test_types[test_type].update(loaded_cfg) self.current_test_types = merged_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