2026-04-16 16:51:05 +08:00
|
|
|
|
# PQ自动化测试配置模块
|
|
|
|
|
|
import json
|
|
|
|
|
|
import copy
|
2026-05-19 11:50:53 +08:00
|
|
|
|
from pathlib import Path
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Pattern 文件读写工具(统一存储格式:settings/patterns/{name}.json)
|
|
|
|
|
|
# =============================================================================
|
2026-04-20 16:44:46 +08:00
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
_PATTERNS_DIR = Path("settings/patterns")
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
def load_pattern_file(filepath) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从 JSON 文件加载 pattern 配置。
|
|
|
|
|
|
|
|
|
|
|
|
文件格式::
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
{
|
2026-04-16 16:51:05 +08:00
|
|
|
|
"pattern_mode": "SolidColor",
|
|
|
|
|
|
"measurement_bit_depth": 8,
|
2026-05-19 11:50:53 +08:00
|
|
|
|
"measurement_max_value": N,
|
|
|
|
|
|
"pattern_params": [[R, G, B], ...]
|
2026-04-16 16:51:05 +08:00
|
|
|
|
}
|
2026-05-19 11:50:53 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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 {
|
2026-04-16 16:51:05 +08:00
|
|
|
|
"pattern_mode": "SolidColor",
|
|
|
|
|
|
"measurement_bit_depth": 8,
|
2026-05-19 11:50:53 +08:00
|
|
|
|
"measurement_max_value": 0,
|
|
|
|
|
|
"pattern_params": [],
|
2026-04-16 16:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 静态默认值 —— 纯常量,永不在运行时修改
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
_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")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-27 11:26:28 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
|
|
|
|
|
|
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",
|
2026-04-16 16:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
# ---- 自定义图案(用户可变)----
|
2026-04-16 16:51:05 +08:00
|
|
|
|
self.custom_pattern = {
|
|
|
|
|
|
"pattern_mode": "SolidColor",
|
|
|
|
|
|
"measurement_bit_depth": 8,
|
|
|
|
|
|
"measurement_max_value": 0,
|
|
|
|
|
|
"pattern_params": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
# ---- 运行态 ----
|
|
|
|
|
|
self.current_test_types = copy.deepcopy(_DEFAULT_TEST_TYPES)
|
2026-04-16 16:51:05 +08:00
|
|
|
|
self.current_test_type = current_test_type
|
2026-05-19 11:50:53 +08:00
|
|
|
|
self.current_pattern = copy.deepcopy(_PATTERN_RGB) # 深拷贝,避免引用污染
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
2026-04-20 16:44:46 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
2026-04-16 16:51:05 +08:00
|
|
|
|
# ========== 获取临时配置(用于 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 模式
|
2026-05-19 11:50:53 +08:00
|
|
|
|
_resolved = get_pattern(mode)
|
|
|
|
|
|
temp_config.current_pattern = _resolved if _resolved["pattern_params"] else copy.deepcopy(_PATTERN_RGB)
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
# 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):
|
2026-05-19 11:50:53 +08:00
|
|
|
|
"""设置当前模式的测试图案(支持所有已注册名称及动态文件 pattern)。"""
|
|
|
|
|
|
pattern = get_pattern(mode)
|
|
|
|
|
|
if not pattern["pattern_params"]:
|
2026-04-16 16:51:05 +08:00
|
|
|
|
return False
|
2026-05-19 11:50:53 +08:00
|
|
|
|
self.current_pattern = pattern # get_pattern 已深拷贝
|
|
|
|
|
|
return True
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
def get_test_item_pattern(self, test_item, test_type=None) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
返回指定测试项目所使用的 pattern 名称。
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
从当前测试类型的 ``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")
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
# ========== 获取 29色名称列表 ==========
|
2026-04-16 16:51:05 +08:00
|
|
|
|
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",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
# ========== 获取 29色的 RGB 值 ==========
|
2026-04-16 16:51:05 +08:00
|
|
|
|
def get_accuracy_color_rgb(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取色准测试的 RGB 值(用于标准值计算)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
list: [(name, r, g, b), ...]
|
|
|
|
|
|
"""
|
|
|
|
|
|
names = self.get_accuracy_color_names()
|
2026-05-19 11:50:53 +08:00
|
|
|
|
rgb_values = _PATTERN_ACCURACY["pattern_params"]
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
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}%")
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
pattern_count = len(_PATTERN_TEMP.get("pattern_params", []))
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
"""获取测试项目的显示名称"""
|
2026-05-19 11:50:53 +08:00
|
|
|
|
_name_map = {
|
|
|
|
|
|
"gamut": "色域",
|
|
|
|
|
|
"gamma": "Gamma",
|
|
|
|
|
|
"eotf": "EOTF",
|
|
|
|
|
|
"cct": "色度一致性",
|
|
|
|
|
|
"contrast": "对比度",
|
|
|
|
|
|
"accuracy": "色准",
|
|
|
|
|
|
}
|
|
|
|
|
|
return [_name_map.get(item, item) for item in test_items]
|
2026-04-16 16:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
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"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 11:50:53 +08:00
|
|
|
|
return config_info
|