优化ucd调用结构
This commit is contained in:
28
app/ucd/__init__.py
Normal file
28
app/ucd/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""UCD 信号发生器 — domain / enum / device / service。
|
||||
|
||||
GUI 与测试代码通常只需::
|
||||
|
||||
from app.ucd import SignalService, UCD323Device, EventBus
|
||||
"""
|
||||
from app.ucd.domain import * # noqa: F403
|
||||
from app.ucd.enum import UCDEnum
|
||||
from app.ucd.device import (
|
||||
DeviceInfo,
|
||||
IUcdDevice,
|
||||
UCD323Device,
|
||||
list_devices,
|
||||
)
|
||||
from app.ucd.service import PatternService, PatternSession, SignalService
|
||||
|
||||
__all__ = [
|
||||
"SignalService",
|
||||
"PatternService",
|
||||
"PatternSession",
|
||||
"UCD323Device",
|
||||
"IUcdDevice",
|
||||
"DeviceInfo",
|
||||
"UCDEnum",
|
||||
"EventBus",
|
||||
"ConnectionChanged",
|
||||
"DeviceKind",
|
||||
]
|
||||
1267
app/ucd/device.py
Normal file
1267
app/ucd/device.py
Normal file
File diff suppressed because it is too large
Load Diff
486
app/ucd/domain.py
Normal file
486
app/ucd/domain.py
Normal file
@@ -0,0 +1,486 @@
|
||||
"""UCD 域层:类型、状态机、事件、字符串与 PQConfig 映射。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Callable
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─── §1 枚举与值对象 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
class DeviceKind(str, Enum):
|
||||
"""连接状态事件所指的设备类型。"""
|
||||
|
||||
UCD = "ucd"
|
||||
CA = "ca"
|
||||
|
||||
|
||||
class Interface(str, Enum):
|
||||
"""UCD 物理输出接口。"""
|
||||
|
||||
HDMI = "HDMI"
|
||||
DP = "DP"
|
||||
USBC = "Type-C"
|
||||
|
||||
|
||||
class ColorFormat(str, Enum):
|
||||
"""像素颜色格式(与 UI 显示字符串解耦的内部表示)。"""
|
||||
|
||||
RGB = "rgb"
|
||||
YCBCR_444 = "ycbcr444"
|
||||
YCBCR_422 = "ycbcr422"
|
||||
YCBCR_420 = "ycbcr420"
|
||||
Y_ONLY = "yonly"
|
||||
IDO_DEFINED = "ido_defined"
|
||||
RAW = "raw"
|
||||
DSC = "dsc"
|
||||
|
||||
|
||||
class Colorimetry(str, Enum):
|
||||
"""色度空间。"""
|
||||
|
||||
SRGB = "sRGB"
|
||||
BT709 = "BT.709"
|
||||
BT601 = "BT.601"
|
||||
BT2020 = "BT.2020"
|
||||
DCI_P3 = "DCI-P3"
|
||||
ADOBE_RGB = "AdobeRGB"
|
||||
|
||||
|
||||
class DynamicRange(str, Enum):
|
||||
FULL = "Full"
|
||||
LIMITED = "Limited"
|
||||
|
||||
|
||||
class TimingStandard(str, Enum):
|
||||
DMT = "dmt"
|
||||
CTA = "cta"
|
||||
CVT = "cvt"
|
||||
OVT = "ovt"
|
||||
|
||||
|
||||
class PatternKind(str, Enum):
|
||||
"""高层图案种类。
|
||||
|
||||
与 UCD 内部 VideoPattern 枚举区分:仅暴露业务上真正用到的几种。
|
||||
"""
|
||||
|
||||
DISABLED = "disabled"
|
||||
SOLID = "solidcolor"
|
||||
SOLID_WHITE = "solidwhite"
|
||||
SOLID_RED = "solidred"
|
||||
SOLID_GREEN = "solidgreen"
|
||||
SOLID_BLUE = "solidblue"
|
||||
COLOR_BARS = "colorbars"
|
||||
CHESSBOARD = "chessboard"
|
||||
WHITE_VSTRIPS = "whitevstrips"
|
||||
GRADIENT_RGB_STRIPES = "gradientrgbstripes"
|
||||
COLOR_RAMP = "colorramp"
|
||||
COLOR_SQUARES = "coloursquares"
|
||||
MOTION = "motionpattern"
|
||||
SQUARE_WINDOW = "squarewindow"
|
||||
IMAGE = "image" # 来自文件路径
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SignalFormat:
|
||||
"""信号格式(color_info 部分)。"""
|
||||
|
||||
color_format: ColorFormat
|
||||
colorimetry: Colorimetry
|
||||
bpc: int # 8 / 10 / 12
|
||||
dynamic_range: DynamicRange = DynamicRange.FULL
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TimingSpec:
|
||||
"""显示 Timing 描述。"""
|
||||
|
||||
standard: TimingStandard
|
||||
width: int
|
||||
height: int
|
||||
refresh_hz: float
|
||||
|
||||
def __str__(self) -> str: # 便于日志
|
||||
return f"{self.standard.value.upper()} {self.width}x{self.height}@{self.refresh_hz:g}Hz"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatternSpec:
|
||||
"""图案描述。
|
||||
|
||||
联合字段含义:
|
||||
SOLID → solid_rgb=(r,g,b)
|
||||
IMAGE → image_path
|
||||
其它预定义图案 → extras 视具体类型
|
||||
"""
|
||||
|
||||
kind: PatternKind
|
||||
solid_rgb: tuple[int, int, int] | None = None
|
||||
image_path: str | None = None
|
||||
extras: tuple = field(default_factory=tuple)
|
||||
|
||||
|
||||
# ─── §2 状态机 ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class UcdState(Enum):
|
||||
CLOSED = 0
|
||||
OPENED = 1 # 设备已打开,未配置信号
|
||||
CONFIGURED = 2 # SignalFormat + Timing 已写入,未 apply
|
||||
APPLIED = 3 # 已 apply,硬件正在输出
|
||||
|
||||
|
||||
_ALLOWED: dict[UcdState, set[UcdState]] = {
|
||||
UcdState.CLOSED: {UcdState.OPENED, UcdState.CLOSED},
|
||||
UcdState.OPENED: {UcdState.CONFIGURED, UcdState.CLOSED, UcdState.OPENED},
|
||||
UcdState.CONFIGURED: {UcdState.CONFIGURED, UcdState.APPLIED, UcdState.OPENED, UcdState.CLOSED},
|
||||
UcdState.APPLIED: {UcdState.CONFIGURED, UcdState.APPLIED, UcdState.OPENED, UcdState.CLOSED},
|
||||
}
|
||||
|
||||
|
||||
def assert_transition(curr: UcdState, nxt: UcdState) -> None:
|
||||
"""校验状态转移。非法转移抛 :class:`UcdStateError`。"""
|
||||
if nxt not in _ALLOWED[curr]:
|
||||
raise UcdStateError(f"非法状态转移: {curr.name} -> {nxt.name}")
|
||||
|
||||
|
||||
# ─── §3 错误体系 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class UcdError(Exception):
|
||||
"""UCD 控制相关错误的基类。"""
|
||||
|
||||
|
||||
class UcdNotConnected(UcdError):
|
||||
"""设备未打开/未连接。"""
|
||||
|
||||
|
||||
class UcdStateError(UcdError):
|
||||
"""状态机非法转移或前置条件不满足。"""
|
||||
|
||||
|
||||
class UcdConfigError(UcdError):
|
||||
"""业务配置参数不合法(如不支持的 color_format、解析失败)。"""
|
||||
|
||||
|
||||
class UcdApplyFailed(UcdError):
|
||||
"""SDK ``apply`` 返回失败或超时。"""
|
||||
|
||||
|
||||
class UcdSdkError(UcdError):
|
||||
"""UniTAP SDK 内部异常的包装;原始异常保存在 ``__cause__``。"""
|
||||
|
||||
|
||||
# ─── §4 事件总线 ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UcdEvent:
|
||||
"""事件基类。"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConnectionChanged(UcdEvent):
|
||||
device: DeviceKind
|
||||
connected: bool
|
||||
serial: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SignalApplied(UcdEvent):
|
||||
signal: SignalFormat
|
||||
timing: TimingSpec
|
||||
format_changed: bool # 与上一次 apply 相比 (signal,timing) 是否改变
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatternApplied(UcdEvent):
|
||||
pattern: PatternSpec
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""极简同步事件总线(按事件类型分发)。
|
||||
|
||||
单线程使用;如需跨线程派发,由订阅者自行 marshal 到 UI 线程。
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._subs: dict[type, list[Callable[[Any], None]]] = {}
|
||||
|
||||
def subscribe(self, evt_type: type, cb: Callable[[Any], None]) -> None:
|
||||
self._subs.setdefault(evt_type, []).append(cb)
|
||||
|
||||
def publish(self, evt: UcdEvent) -> None:
|
||||
for cb in self._subs.get(type(evt), []):
|
||||
try:
|
||||
cb(evt)
|
||||
except Exception: # noqa: BLE001 - 订阅者错误不应影响发布者
|
||||
log.exception("UCD 事件处理器抛出异常: %r", evt)
|
||||
|
||||
|
||||
# ─── §5 业务字符串解析 / 映射 ────────────────────────────────────
|
||||
|
||||
|
||||
_OUTPUT_FORMAT_TO_COLOR_FORMAT: dict[str, ColorFormat] = {
|
||||
"RGB": ColorFormat.RGB,
|
||||
"YCbCr 4:4:4": ColorFormat.YCBCR_444,
|
||||
"YCbCr 4:2:2": ColorFormat.YCBCR_422,
|
||||
"YCbCr 4:2:0": ColorFormat.YCBCR_420,
|
||||
"Y Only": ColorFormat.Y_ONLY,
|
||||
"IDO Defined": ColorFormat.IDO_DEFINED,
|
||||
"RAW": ColorFormat.RAW,
|
||||
"DSC": ColorFormat.DSC,
|
||||
}
|
||||
|
||||
_COLOR_SPACE_TO_COLORIMETRY: dict[str, Colorimetry] = {
|
||||
"sRGB": Colorimetry.SRGB,
|
||||
"BT.709": Colorimetry.BT709,
|
||||
"BT.601": Colorimetry.BT601,
|
||||
"BT.2020": Colorimetry.BT2020,
|
||||
"DCI-P3": Colorimetry.DCI_P3,
|
||||
"AdobeRGB": Colorimetry.ADOBE_RGB,
|
||||
}
|
||||
|
||||
_BIT_DEPTH_STR_TO_BPC: dict[str, int] = {
|
||||
"8bit": 8,
|
||||
"10bit": 10,
|
||||
"12bit": 12,
|
||||
}
|
||||
|
||||
_DATA_RANGE_TO_DYNAMIC_RANGE: dict[str, DynamicRange] = {
|
||||
"Full": DynamicRange.FULL,
|
||||
"Limited": DynamicRange.LIMITED,
|
||||
}
|
||||
|
||||
|
||||
def output_format_to_color_format(s: str) -> ColorFormat:
|
||||
"""显示用 ``"YCbCr 4:4:4"`` → :class:`ColorFormat`。未知值视为 RGB。"""
|
||||
return _OUTPUT_FORMAT_TO_COLOR_FORMAT.get(s, ColorFormat.RGB)
|
||||
|
||||
|
||||
def color_space_to_colorimetry(s: str) -> Colorimetry:
|
||||
"""显示用 ``"BT.709"`` → :class:`Colorimetry`。未知抛 :class:`UcdConfigError`。"""
|
||||
if s in _COLOR_SPACE_TO_COLORIMETRY:
|
||||
return _COLOR_SPACE_TO_COLORIMETRY[s]
|
||||
raise UcdConfigError(f"未知色彩空间: {s!r}")
|
||||
|
||||
|
||||
def bit_depth_str_to_bpc(s: str) -> int:
|
||||
"""``"10bit"`` → 10。未知抛 :class:`UcdConfigError`。"""
|
||||
if s in _BIT_DEPTH_STR_TO_BPC:
|
||||
return _BIT_DEPTH_STR_TO_BPC[s]
|
||||
raise UcdConfigError(f"未知位深: {s!r}")
|
||||
|
||||
|
||||
def data_range_to_dynamic_range(s: str) -> DynamicRange:
|
||||
if s in _DATA_RANGE_TO_DYNAMIC_RANGE:
|
||||
return _DATA_RANGE_TO_DYNAMIC_RANGE[s]
|
||||
raise UcdConfigError(f"未知数据范围: {s!r}")
|
||||
|
||||
|
||||
def is_ycbcr(color_format: ColorFormat | str | None) -> bool:
|
||||
"""判断输出是否为 YCbCr 系列。接受 :class:`ColorFormat` 或 UI 字符串。"""
|
||||
if color_format is None:
|
||||
return False
|
||||
if isinstance(color_format, ColorFormat):
|
||||
return color_format in {
|
||||
ColorFormat.YCBCR_444,
|
||||
ColorFormat.YCBCR_422,
|
||||
ColorFormat.YCBCR_420,
|
||||
}
|
||||
return "YCbCr" in str(color_format)
|
||||
|
||||
|
||||
def parse_timing_str(timing_str: str) -> TimingSpec:
|
||||
"""解析 ``"DMT 3840x2160@60Hz"`` 风格的字符串为 :class:`TimingSpec`。
|
||||
|
||||
宽容处理空格 / 大小写 / ``Hz`` 后缀大小写。
|
||||
解析失败抛 :class:`UcdConfigError`。
|
||||
"""
|
||||
if not isinstance(timing_str, str):
|
||||
raise UcdConfigError(f"timing_str 必须是字符串: {timing_str!r}")
|
||||
|
||||
s = " ".join(timing_str.strip().split())
|
||||
s = s.replace(" x", "x").replace("x ", "x")
|
||||
|
||||
parts = s.split(" ", 1)
|
||||
if len(parts) < 2:
|
||||
raise UcdConfigError(f"无法解析 timing: {timing_str!r}")
|
||||
type_str, rest = parts[0].strip().upper(), parts[1].strip()
|
||||
|
||||
if "@" not in rest:
|
||||
raise UcdConfigError(f"无法解析 timing (缺少 '@'): {timing_str!r}")
|
||||
left, right = (p.strip() for p in rest.split("@", 1))
|
||||
|
||||
if "x" not in left:
|
||||
raise UcdConfigError(f"无法解析分辨率 (缺少 'x'): {timing_str!r}")
|
||||
wh = left.split("x")
|
||||
if len(wh) != 2:
|
||||
raise UcdConfigError(f"无法解析分辨率: {timing_str!r}")
|
||||
try:
|
||||
width, height = int(wh[0]), int(wh[1])
|
||||
except ValueError as exc:
|
||||
raise UcdConfigError(f"分辨率数字解析失败: {timing_str!r}") from exc
|
||||
|
||||
hz_str = right.replace("Hz", "").replace("HZ", "").strip()
|
||||
try:
|
||||
refresh_hz = float(hz_str)
|
||||
except ValueError as exc:
|
||||
raise UcdConfigError(f"刷新率解析失败: {timing_str!r}") from exc
|
||||
|
||||
try:
|
||||
standard = TimingStandard(type_str.lower())
|
||||
except ValueError as exc:
|
||||
raise UcdConfigError(f"未知的分辨率类型: {type_str!r}") from exc
|
||||
|
||||
return TimingSpec(
|
||||
standard=standard,
|
||||
width=width,
|
||||
height=height,
|
||||
refresh_hz=refresh_hz,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# §1
|
||||
"DeviceKind",
|
||||
"Interface",
|
||||
"ColorFormat",
|
||||
"Colorimetry",
|
||||
"DynamicRange",
|
||||
"TimingStandard",
|
||||
"PatternKind",
|
||||
"SignalFormat",
|
||||
"TimingSpec",
|
||||
"PatternSpec",
|
||||
# §2
|
||||
"UcdState",
|
||||
"assert_transition",
|
||||
# §3
|
||||
"UcdError",
|
||||
"UcdNotConnected",
|
||||
"UcdStateError",
|
||||
"UcdConfigError",
|
||||
"UcdApplyFailed",
|
||||
"UcdSdkError",
|
||||
# §4
|
||||
"UcdEvent",
|
||||
"ConnectionChanged",
|
||||
"SignalApplied",
|
||||
"PatternApplied",
|
||||
"EventBus",
|
||||
# §5
|
||||
"output_format_to_color_format",
|
||||
"color_space_to_colorimetry",
|
||||
"bit_depth_str_to_bpc",
|
||||
"data_range_to_dynamic_range",
|
||||
"is_ycbcr",
|
||||
"parse_timing_str",
|
||||
]
|
||||
|
||||
|
||||
# --- PQConfig / pattern 映射 ---
|
||||
|
||||
# PQ pattern_mode 字符串 → PatternKind(大小写不敏感)
|
||||
_PQ_PATTERN_MODE_TO_KIND: dict[str, PatternKind] = {
|
||||
"disabled": PatternKind.DISABLED,
|
||||
"solidcolor": PatternKind.SOLID,
|
||||
"solidwhite": PatternKind.SOLID_WHITE,
|
||||
"solidred": PatternKind.SOLID_RED,
|
||||
"solidgreen": PatternKind.SOLID_GREEN,
|
||||
"solidblue": PatternKind.SOLID_BLUE,
|
||||
"colorbars": PatternKind.COLOR_BARS,
|
||||
"chessboard": PatternKind.CHESSBOARD,
|
||||
"whitevstrips": PatternKind.WHITE_VSTRIPS,
|
||||
"gradientrgbstripes": PatternKind.GRADIENT_RGB_STRIPES,
|
||||
"colorramp": PatternKind.COLOR_RAMP,
|
||||
"coloursquares": PatternKind.COLOR_SQUARES,
|
||||
"motionpattern": PatternKind.MOTION,
|
||||
"squarewindow": PatternKind.SQUARE_WINDOW,
|
||||
}
|
||||
|
||||
|
||||
def pattern_mode_to_kind(pattern_mode: str) -> PatternKind:
|
||||
key = (pattern_mode or "solidcolor").strip().lower()
|
||||
kind = _PQ_PATTERN_MODE_TO_KIND.get(key)
|
||||
if kind is None:
|
||||
raise UcdConfigError(f"不支持的 pattern_mode: {pattern_mode!r}")
|
||||
return kind
|
||||
|
||||
|
||||
def build_profile_from_config(config, test_type: str | None = None):
|
||||
"""从 PQConfig 当前 test_type 条目构建 SignalFormat + TimingSpec。"""
|
||||
test_type = test_type or config.current_test_type
|
||||
profile = config.current_test_types[test_type]
|
||||
signal = build_signal_format_from_profile(
|
||||
color_space=profile["colorimetry"],
|
||||
color_format=profile["color_format"],
|
||||
bpc=int(profile["bpc"]),
|
||||
data_range=profile.get("data_range", "Full"),
|
||||
)
|
||||
timing = build_timing(profile["timing"])
|
||||
return signal, timing
|
||||
|
||||
|
||||
def build_pattern_spec(config, params: list[int] | None = None) -> PatternSpec:
|
||||
"""将 PQConfig 当前 pattern 与一组参数转为 :class:`PatternSpec`。"""
|
||||
pattern_mode = config.current_pattern["pattern_mode"]
|
||||
kind = pattern_mode_to_kind(pattern_mode)
|
||||
if params is None:
|
||||
params = config.current_pattern.get("pattern_params", [[]])[0]
|
||||
if kind is PatternKind.SOLID and params and len(params) >= 3:
|
||||
return PatternSpec(
|
||||
kind=kind,
|
||||
solid_rgb=(int(params[0]), int(params[1]), int(params[2])),
|
||||
)
|
||||
if params:
|
||||
return PatternSpec(kind=kind, extras=tuple(int(v) for v in params))
|
||||
return PatternSpec(kind=kind)
|
||||
|
||||
def build_signal_format(
|
||||
*,
|
||||
color_space: str,
|
||||
output_format: str,
|
||||
bit_depth: str,
|
||||
data_range: str = "Full",
|
||||
) -> SignalFormat:
|
||||
return SignalFormat(
|
||||
color_format=output_format_to_color_format(output_format),
|
||||
colorimetry=color_space_to_colorimetry(color_space),
|
||||
bpc=bit_depth_str_to_bpc(bit_depth),
|
||||
dynamic_range=data_range_to_dynamic_range(data_range),
|
||||
)
|
||||
|
||||
|
||||
def build_signal_format_from_profile(
|
||||
*,
|
||||
color_space: str,
|
||||
color_format: str,
|
||||
bpc: int,
|
||||
data_range: str = "Full",
|
||||
) -> SignalFormat:
|
||||
return build_signal_format(
|
||||
color_space=color_space,
|
||||
output_format=color_format,
|
||||
bit_depth=f"{int(bpc)}bit",
|
||||
data_range=data_range,
|
||||
)
|
||||
|
||||
|
||||
def build_timing(timing_str: str) -> TimingSpec:
|
||||
return parse_timing_str(timing_str)
|
||||
|
||||
|
||||
def solid_rgb_pattern(rgb: tuple[int, int, int] | list[int]) -> PatternSpec:
|
||||
r, g, b = rgb[0], rgb[1], rgb[2]
|
||||
return PatternSpec(kind=PatternKind.SOLID, solid_rgb=(int(r), int(g), int(b)))
|
||||
|
||||
|
||||
def image_pattern(path: str) -> PatternSpec:
|
||||
return PatternSpec(kind=PatternKind.IMAGE, image_path=path)
|
||||
625
app/ucd/enum.py
Normal file
625
app/ucd/enum.py
Normal file
@@ -0,0 +1,625 @@
|
||||
"""UCD SDK 枚举与 UI/SDK 字符串映射。"""
|
||||
|
||||
from enum import IntEnum
|
||||
import UniTAP
|
||||
|
||||
|
||||
class UCDEnum:
|
||||
class ColorInfo:
|
||||
"""
|
||||
|
||||
Class contains information of frame `ColorFormat`, `DynamicRange`, `Colorimetry`.
|
||||
|
||||
"""
|
||||
|
||||
class ColorFormat(IntEnum):
|
||||
"""
|
||||
Contains values of possible color format.
|
||||
"""
|
||||
|
||||
CF_NONE = 0
|
||||
CF_UNKNOWN = 1
|
||||
CF_RGB = 2
|
||||
CF_YCbCr_422 = 3
|
||||
CF_YCbCr_444 = 4
|
||||
CF_YCbCr_420 = 5
|
||||
CF_IDO_DEFINED = 6
|
||||
CF_Y_ONLY = 7
|
||||
CF_RAW = 8
|
||||
CF_DSC = 9
|
||||
|
||||
class DynamicRange(IntEnum):
|
||||
"""
|
||||
Contains values of possible dynamic range.
|
||||
"""
|
||||
|
||||
DR_UNKNOWN = -1
|
||||
DR_VESA = 0
|
||||
DR_CTA = 1
|
||||
|
||||
class Colorimetry(IntEnum):
|
||||
"""
|
||||
Contains values of possible colorimetry.
|
||||
"""
|
||||
|
||||
CM_NONE = 0
|
||||
CM_RESERVED = 1
|
||||
CM_sRGB = 2
|
||||
CM_SMPTE_170M = 3
|
||||
CM_ITUR_BT601 = 4
|
||||
CM_ITUR_BT709 = 5
|
||||
CM_xvYCC601 = 6
|
||||
CM_xvYCC709 = 7
|
||||
CM_sYCC601 = 8
|
||||
CM_AdobeYCC601 = 9
|
||||
CM_AdobeRGB = 10
|
||||
CM_ITUR_BT2020_YcCbcCrc = 11
|
||||
CM_ITUR_BT2020_YCbCr = 12
|
||||
CM_ITUR_BT2020_RGB = 13
|
||||
CM_RGB_WIDE_GAMUT_FIX = 14
|
||||
CM_RGB_WIDE_GAMUT_FLT = 15
|
||||
CM_DCI_P3 = 16
|
||||
CM_DICOM_1_4_GRAY_SCALE = 17
|
||||
CM_CUSTOM_COLOR_PROFILE = 18
|
||||
|
||||
CM_opYCC601 = CM_AdobeYCC601
|
||||
CM_opRGB = CM_AdobeRGB
|
||||
|
||||
# 颜色格式映射 - 支持不区分大小写的字符串匹配
|
||||
@staticmethod
|
||||
def get_color_format(format_str):
|
||||
format_map = {
|
||||
"none": UniTAP.ColorInfo.ColorFormat.CF_NONE,
|
||||
"unknown": UniTAP.ColorInfo.ColorFormat.CF_UNKNOWN,
|
||||
"rgb": UniTAP.ColorInfo.ColorFormat.CF_RGB,
|
||||
"ycbcr422": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_422,
|
||||
"ycbcr444": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_444,
|
||||
"ycbcr420": UniTAP.ColorInfo.ColorFormat.CF_YCbCr_420,
|
||||
"ido_defined": UniTAP.ColorInfo.ColorFormat.CF_IDO_DEFINED,
|
||||
"yonly": UniTAP.ColorInfo.ColorFormat.CF_Y_ONLY,
|
||||
"raw": UniTAP.ColorInfo.ColorFormat.CF_RAW,
|
||||
"dsc": UniTAP.ColorInfo.ColorFormat.CF_DSC,
|
||||
}
|
||||
if not format_str:
|
||||
return None
|
||||
return format_map.get(format_str.lower(), None)
|
||||
|
||||
# 色度映射 - 支持不区分大小写的字符串匹配
|
||||
@staticmethod
|
||||
def get_colorimetry(colorimetry_str):
|
||||
colorimetry_map = {
|
||||
"none": UniTAP.ColorInfo.Colorimetry.CM_NONE,
|
||||
"reserved": UniTAP.ColorInfo.Colorimetry.CM_RESERVED,
|
||||
"srgb": UniTAP.ColorInfo.Colorimetry.CM_sRGB,
|
||||
"smpte170m": UniTAP.ColorInfo.Colorimetry.CM_SMPTE_170M,
|
||||
"bt601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
|
||||
"bt709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
|
||||
"xvycc601": UniTAP.ColorInfo.Colorimetry.CM_xvYCC601,
|
||||
"xvycc709": UniTAP.ColorInfo.Colorimetry.CM_xvYCC709,
|
||||
"sycc601": UniTAP.ColorInfo.Colorimetry.CM_sYCC601,
|
||||
"adobeycc601": UniTAP.ColorInfo.Colorimetry.CM_AdobeYCC601,
|
||||
"adobergb": UniTAP.ColorInfo.Colorimetry.CM_AdobeRGB,
|
||||
"bt2020yccbccrc": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YcCbcCrc,
|
||||
"bt2020ycbcr": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr,
|
||||
"bt2020rgb": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
|
||||
"rgbwidegamutfix": UniTAP.ColorInfo.Colorimetry.CM_RGB_WIDE_GAMUT_FIX,
|
||||
"rgbwidegamutflt": UniTAP.ColorInfo.Colorimetry.CM_RGB_WIDE_GAMUT_FLT,
|
||||
"dcip3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
|
||||
"dicom14grayscale": UniTAP.ColorInfo.Colorimetry.CM_DICOM_1_4_GRAY_SCALE,
|
||||
"customcolorprofile": UniTAP.ColorInfo.Colorimetry.CM_CUSTOM_COLOR_PROFILE,
|
||||
"opycc601": UniTAP.ColorInfo.Colorimetry.CM_opYCC601,
|
||||
"oprgb": UniTAP.ColorInfo.Colorimetry.CM_opRGB,
|
||||
}
|
||||
if not colorimetry_str:
|
||||
return None
|
||||
# Normalize: strip hyphens, spaces, dots, underscores so that
|
||||
# "DCI-P3" → "dcip3", "BT.709" → "bt709", "BT.2020 YCbCr" → "bt2020ycbcr"
|
||||
normalized = (
|
||||
colorimetry_str.lower()
|
||||
.replace("-", "")
|
||||
.replace(" ", "")
|
||||
.replace(".", "")
|
||||
.replace("_", "")
|
||||
)
|
||||
return colorimetry_map.get(normalized, colorimetry_map.get(colorimetry_str.lower(), None))
|
||||
|
||||
class VideoPatternInfo:
|
||||
class VideoPattern(IntEnum):
|
||||
"""
|
||||
Class `VideoPattern` contains all possible variants of patterns which can be set in the function `set_pattern`.
|
||||
"""
|
||||
|
||||
Disabled = 0
|
||||
ColorBars = 1
|
||||
Chessboard = 2
|
||||
SolidColor = 3
|
||||
SolidWhite = 4
|
||||
SolidRed = 5
|
||||
SolidGreen = 6
|
||||
SolidBlue = 7
|
||||
WhiteVStrips = 8
|
||||
GradientRGBStripes = 9
|
||||
ColorRamp = 10
|
||||
ColorSquares = 11
|
||||
MotionPattern = 12
|
||||
SquareWindow = 15
|
||||
|
||||
class VideoPatternParams(IntEnum):
|
||||
"""
|
||||
Class `VideoPatternParams` contains all possible variants of parameters which can be set in the function `set_pattern_params`.
|
||||
"""
|
||||
|
||||
SolidColor = 3
|
||||
WhiteVStrips = 8
|
||||
GradientRGBStripes = 9
|
||||
MotionPattern = 12
|
||||
SquareWindow = 15
|
||||
|
||||
@staticmethod
|
||||
def get_video_pattern(pattern_str):
|
||||
pattern_map = {
|
||||
"disabled": UniTAP.VideoPattern.Disabled,
|
||||
"colorbars": UniTAP.VideoPattern.ColorBars,
|
||||
"chessboard": UniTAP.VideoPattern.Chessboard,
|
||||
"solidcolor": UniTAP.VideoPattern.SolidColor,
|
||||
"solidwhite": UniTAP.VideoPattern.SolidWhite,
|
||||
"solidred": UniTAP.VideoPattern.SolidRed,
|
||||
"solidgreen": UniTAP.VideoPattern.SolidGreen,
|
||||
"solidblue": UniTAP.VideoPattern.SolidBlue,
|
||||
"whitevstrips": UniTAP.VideoPattern.WhiteVStrips,
|
||||
"gradientrgbstripes": UniTAP.VideoPattern.GradientRGBStripes,
|
||||
"colorramp": UniTAP.VideoPattern.ColorRamp,
|
||||
"coloursquares": UniTAP.VideoPattern.ColorSquares,
|
||||
"motionpattern": UniTAP.VideoPattern.MotionPattern,
|
||||
"squarewindow": UniTAP.VideoPattern.SquareWindow,
|
||||
}
|
||||
if not pattern_str:
|
||||
return None
|
||||
return pattern_map.get(pattern_str.lower(), None)
|
||||
|
||||
class TimingInfo:
|
||||
class ResolutionType(IntEnum):
|
||||
"""
|
||||
分辨率类型枚举,包含DMT、CTA、CVT和OVT四种类型
|
||||
"""
|
||||
|
||||
DMT = 0 # VESA Display Monitor Timing
|
||||
CTA = 1 # Consumer Technology Association
|
||||
CVT = 2 # Coordinated Video Timing
|
||||
OVT = 3 # Other Video Timing
|
||||
|
||||
# 分辨率类型映射
|
||||
resolution_type_map = {
|
||||
"dmt": ResolutionType.DMT,
|
||||
"cta": ResolutionType.CTA,
|
||||
"cvt": ResolutionType.CVT,
|
||||
"ovt": ResolutionType.OVT,
|
||||
}
|
||||
|
||||
# DMT分辨率ID映射
|
||||
dmt_resolution_map = {
|
||||
9: {"width": 800, "height": 600, "refresh_rate": 60.317, "id_hex": "9h"},
|
||||
14: {"width": 848, "height": 480, "refresh_rate": 60.0, "id_hex": "Eh"},
|
||||
16: {"width": 1024, "height": 768, "refresh_rate": 60.0, "id_hex": "10h"},
|
||||
23: {"width": 1280, "height": 768, "refresh_rate": 60.0, "id_hex": "17h"},
|
||||
27: {
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"refresh_rate": 60.0,
|
||||
"id_hex": "1Bh",
|
||||
"note": "RB1",
|
||||
},
|
||||
28: {"width": 1280, "height": 800, "refresh_rate": 60.0, "id_hex": "1Ch"},
|
||||
32: {"width": 1280, "height": 960, "refresh_rate": 60.0, "id_hex": "20h"},
|
||||
35: {"width": 1280, "height": 1024, "refresh_rate": 60.0, "id_hex": "23h"},
|
||||
39: {"width": 1360, "height": 768, "refresh_rate": 60.0, "id_hex": "27h"},
|
||||
41: {
|
||||
"width": 1400,
|
||||
"height": 1050,
|
||||
"refresh_rate": 60.0,
|
||||
"id_hex": "29h",
|
||||
"note": "RB1",
|
||||
},
|
||||
42: {"width": 1400, "height": 1050, "refresh_rate": 60.0, "id_hex": "2Ah"},
|
||||
47: {"width": 1440, "height": 900, "refresh_rate": 59.887, "id_hex": "2Fh"},
|
||||
51: {"width": 1600, "height": 1200, "refresh_rate": 60.0, "id_hex": "33h"},
|
||||
57: {
|
||||
"width": 1680,
|
||||
"height": 1050,
|
||||
"refresh_rate": 60.0,
|
||||
"id_hex": "39h",
|
||||
"note": "RB1",
|
||||
},
|
||||
58: {"width": 1680, "height": 1050, "refresh_rate": 60.0, "id_hex": "3Ah"},
|
||||
62: {"width": 1792, "height": 1344, "refresh_rate": 60.0, "id_hex": "3Eh"},
|
||||
65: {"width": 1856, "height": 1392, "refresh_rate": 60.0, "id_hex": "41h"},
|
||||
69: {"width": 1920, "height": 1200, "refresh_rate": 60.0, "id_hex": "45h"},
|
||||
73: {"width": 1920, "height": 1440, "refresh_rate": 60.0, "id_hex": "49h"},
|
||||
76: {
|
||||
"width": 2560,
|
||||
"height": 1600,
|
||||
"refresh_rate": 60.0,
|
||||
"id_hex": "4Ch",
|
||||
"note": "RB1",
|
||||
},
|
||||
77: {"width": 2560, "height": 1600, "refresh_rate": 60.0, "id_hex": "4Dh"},
|
||||
82: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "id_hex": "52h"},
|
||||
}
|
||||
|
||||
# CTA分辨率ID映射
|
||||
cta_resolution_map = {
|
||||
1: {"width": 640, "height": 480, "refresh_rate": 59.94, "vic": 1},
|
||||
2: {"width": 720, "height": 480, "refresh_rate": 59.94, "vic": 2},
|
||||
3: {"width": 720, "height": 480, "refresh_rate": 59.94, "vic": 3},
|
||||
4: {"width": 1280, "height": 720, "refresh_rate": 60.0, "vic": 4},
|
||||
8: {"width": 1440, "height": 240, "refresh_rate": 59.826, "vic": 8},
|
||||
9: {"width": 1440, "height": 240, "refresh_rate": 60.054, "vic": 9},
|
||||
12: {"width": 2880, "height": 240, "refresh_rate": 59.826, "vic": 12},
|
||||
13: {"width": 2880, "height": 240, "refresh_rate": 59.826, "vic": 13},
|
||||
14: {"width": 1440, "height": 480, "refresh_rate": 59.94, "vic": 14},
|
||||
15: {"width": 1440, "height": 480, "refresh_rate": 59.94, "vic": 15},
|
||||
16: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "vic": 16},
|
||||
17: {"width": 720, "height": 576, "refresh_rate": 50.0, "vic": 17},
|
||||
18: {"width": 720, "height": 576, "refresh_rate": 50.0, "vic": 18},
|
||||
19: {"width": 1280, "height": 720, "refresh_rate": 50.0, "vic": 19},
|
||||
23: {"width": 1440, "height": 288, "refresh_rate": 49.761, "vic": 23},
|
||||
24: {"width": 1440, "height": 288, "refresh_rate": 49.761, "vic": 24},
|
||||
27: {"width": 2880, "height": 288, "refresh_rate": 49.761, "vic": 27},
|
||||
28: {"width": 2880, "height": 288, "refresh_rate": 49.761, "vic": 28},
|
||||
29: {"width": 1440, "height": 576, "refresh_rate": 50.0, "vic": 29},
|
||||
30: {"width": 1440, "height": 576, "refresh_rate": 50.0, "vic": 30},
|
||||
31: {"width": 1920, "height": 1080, "refresh_rate": 50.0, "vic": 31},
|
||||
32: {"width": 1920, "height": 1080, "refresh_rate": 24.0, "vic": 32},
|
||||
33: {"width": 1920, "height": 1080, "refresh_rate": 25.0, "vic": 33},
|
||||
34: {"width": 1920, "height": 1080, "refresh_rate": 30.0, "vic": 34},
|
||||
35: {"width": 2880, "height": 480, "refresh_rate": 59.94, "vic": 35},
|
||||
36: {"width": 2880, "height": 480, "refresh_rate": 59.94, "vic": 36},
|
||||
37: {"width": 2880, "height": 576, "refresh_rate": 50.0, "vic": 37},
|
||||
38: {"width": 2880, "height": 576, "refresh_rate": 50.0, "vic": 38},
|
||||
41: {"width": 1280, "height": 720, "refresh_rate": 100.0, "vic": 41},
|
||||
42: {"width": 720, "height": 576, "refresh_rate": 100.0, "vic": 42},
|
||||
43: {"width": 720, "height": 576, "refresh_rate": 100.0, "vic": 43},
|
||||
47: {"width": 1280, "height": 720, "refresh_rate": 120.0, "vic": 47},
|
||||
48: {"width": 720, "height": 480, "refresh_rate": 120.0, "vic": 48},
|
||||
49: {"width": 720, "height": 480, "refresh_rate": 119.88, "vic": 49},
|
||||
52: {"width": 720, "height": 576, "refresh_rate": 200.0, "vic": 52},
|
||||
53: {"width": 720, "height": 576, "refresh_rate": 200.0, "vic": 53},
|
||||
56: {"width": 720, "height": 480, "refresh_rate": 239.76, "vic": 56},
|
||||
57: {"width": 720, "height": 480, "refresh_rate": 239.76, "vic": 57},
|
||||
60: {"width": 1280, "height": 720, "refresh_rate": 24.0, "vic": 60},
|
||||
61: {"width": 1280, "height": 720, "refresh_rate": 25.0, "vic": 61},
|
||||
62: {"width": 1280, "height": 720, "refresh_rate": 30.0, "vic": 62},
|
||||
63: {"width": 1920, "height": 1080, "refresh_rate": 120.0, "vic": 63},
|
||||
64: {"width": 1920, "height": 1080, "refresh_rate": 100.0, "vic": 64},
|
||||
65: {"width": 1280, "height": 720, "refresh_rate": 24.0, "vic": 65},
|
||||
66: {"width": 1280, "height": 720, "refresh_rate": 25.0, "vic": 66},
|
||||
67: {"width": 1280, "height": 720, "refresh_rate": 30.0, "vic": 67},
|
||||
68: {"width": 1280, "height": 720, "refresh_rate": 50.0, "vic": 68},
|
||||
69: {"width": 1280, "height": 720, "refresh_rate": 60.0, "vic": 69},
|
||||
70: {"width": 1280, "height": 720, "refresh_rate": 100.0, "vic": 70},
|
||||
71: {"width": 1280, "height": 720, "refresh_rate": 120.0, "vic": 71},
|
||||
72: {"width": 1920, "height": 1080, "refresh_rate": 24.0, "vic": 72},
|
||||
73: {"width": 1920, "height": 1080, "refresh_rate": 25.0, "vic": 73},
|
||||
74: {"width": 1920, "height": 1080, "refresh_rate": 30.0, "vic": 74},
|
||||
75: {"width": 1920, "height": 1080, "refresh_rate": 50.0, "vic": 75},
|
||||
76: {"width": 1920, "height": 1080, "refresh_rate": 60.0, "vic": 76},
|
||||
77: {"width": 1920, "height": 1080, "refresh_rate": 100.0, "vic": 77},
|
||||
78: {"width": 1920, "height": 1080, "refresh_rate": 120.0, "vic": 78},
|
||||
79: {"width": 1680, "height": 720, "refresh_rate": 24.0, "vic": 79},
|
||||
80: {"width": 1680, "height": 720, "refresh_rate": 25.0, "vic": 80},
|
||||
81: {"width": 1680, "height": 720, "refresh_rate": 30.0, "vic": 81},
|
||||
82: {"width": 1680, "height": 720, "refresh_rate": 50.0, "vic": 82},
|
||||
83: {"width": 1680, "height": 720, "refresh_rate": 60.0, "vic": 83},
|
||||
84: {"width": 1680, "height": 720, "refresh_rate": 100.0, "vic": 84},
|
||||
85: {"width": 1680, "height": 720, "refresh_rate": 120.0, "vic": 85},
|
||||
86: {"width": 2560, "height": 1080, "refresh_rate": 24.0, "vic": 86},
|
||||
87: {"width": 2560, "height": 1080, "refresh_rate": 25.0, "vic": 87},
|
||||
88: {"width": 2560, "height": 1080, "refresh_rate": 30.0, "vic": 88},
|
||||
89: {"width": 2560, "height": 1080, "refresh_rate": 50.0, "vic": 89},
|
||||
90: {"width": 2560, "height": 1080, "refresh_rate": 60.0, "vic": 90},
|
||||
91: {"width": 2560, "height": 1080, "refresh_rate": 100.0, "vic": 91},
|
||||
92: {"width": 2560, "height": 1080, "refresh_rate": 120.0, "vic": 92},
|
||||
93: {"width": 3840, "height": 2160, "refresh_rate": 24.0, "vic": 93},
|
||||
94: {"width": 3840, "height": 2160, "refresh_rate": 25.0, "vic": 94},
|
||||
95: {"width": 3840, "height": 2160, "refresh_rate": 30.0, "vic": 95},
|
||||
96: {"width": 3840, "height": 2160, "refresh_rate": 50.0, "vic": 96},
|
||||
97: {"width": 3840, "height": 2160, "refresh_rate": 60.0, "vic": 97},
|
||||
98: {"width": 4096, "height": 2160, "refresh_rate": 24.0, "vic": 98},
|
||||
99: {"width": 4096, "height": 2160, "refresh_rate": 25.0, "vic": 99},
|
||||
100: {"width": 4096, "height": 2160, "refresh_rate": 30.0, "vic": 100},
|
||||
101: {"width": 4096, "height": 2160, "refresh_rate": 50.0, "vic": 101},
|
||||
102: {"width": 4096, "height": 2160, "refresh_rate": 60.0, "vic": 102},
|
||||
103: {"width": 3840, "height": 2160, "refresh_rate": 24.0, "vic": 103},
|
||||
104: {"width": 3840, "height": 2160, "refresh_rate": 25.0, "vic": 104},
|
||||
105: {"width": 3840, "height": 2160, "refresh_rate": 30.0, "vic": 105},
|
||||
106: {"width": 3840, "height": 2160, "refresh_rate": 50.0, "vic": 106},
|
||||
107: {"width": 3840, "height": 2160, "refresh_rate": 60.0, "vic": 107},
|
||||
108: {"width": 1280, "height": 720, "refresh_rate": 48.0, "vic": 108},
|
||||
109: {"width": 1280, "height": 720, "refresh_rate": 48.0, "vic": 109},
|
||||
110: {"width": 1680, "height": 720, "refresh_rate": 48.0, "vic": 110},
|
||||
111: {"width": 1920, "height": 1080, "refresh_rate": 48.0, "vic": 111},
|
||||
112: {"width": 1920, "height": 1080, "refresh_rate": 48.0, "vic": 112},
|
||||
113: {"width": 2560, "height": 1080, "refresh_rate": 48.0, "vic": 113},
|
||||
114: {"width": 3840, "height": 2160, "refresh_rate": 48.0, "vic": 114},
|
||||
115: {"width": 4096, "height": 2160, "refresh_rate": 48.0, "vic": 115},
|
||||
116: {"width": 3840, "height": 2160, "refresh_rate": 48.0, "vic": 116},
|
||||
117: {"width": 3840, "height": 2160, "refresh_rate": 100.0, "vic": 117},
|
||||
118: {"width": 3840, "height": 2160, "refresh_rate": 120.0, "vic": 118},
|
||||
119: {"width": 3840, "height": 2160, "refresh_rate": 100.0, "vic": 119},
|
||||
120: {"width": 3840, "height": 2160, "refresh_rate": 120.0, "vic": 120},
|
||||
218: {"width": 4096, "height": 2160, "refresh_rate": 100.0, "vic": 218},
|
||||
219: {"width": 4096, "height": 2160, "refresh_rate": 120.0, "vic": 219},
|
||||
}
|
||||
|
||||
# CVT分辨率ID映射
|
||||
cvt_resolution_map = {
|
||||
0: [
|
||||
{"width": 640, "height": 480, "refresh_rate": 60.0},
|
||||
{"width": 768, "height": 480, "refresh_rate": 84.502},
|
||||
{"width": 1024, "height": 640, "refresh_rate": 59.887},
|
||||
{"width": 1152, "height": 720, "refresh_rate": 74.721},
|
||||
{"width": 1280, "height": 768, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 1280, "height": 960, "refresh_rate": 59.939},
|
||||
{"width": 1536, "height": 960, "refresh_rate": 84.884},
|
||||
{"width": 1600, "height": 1200, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 30.0, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 30.0, "note": "RB2"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 60.0, "note": "RB2"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 84.884},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 120.0, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 120.0, "note": "RB2"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB2"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 144.05, "note": "RB3"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 200.07, "note": "RB3"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB1"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB2"},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 239.898, "note": "RB3"},
|
||||
{"width": 1920, "height": 1440, "refresh_rate": 59.974, "note": "RB1"},
|
||||
{"width": 2048, "height": 1280, "refresh_rate": 59.922, "note": "RB1"},
|
||||
{"width": 2048, "height": 1536, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 2128, "height": 1200, "refresh_rate": 59.946},
|
||||
{"width": 2456, "height": 1536, "refresh_rate": 50.0},
|
||||
{"width": 2456, "height": 1536, "refresh_rate": 74.935},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 60.0},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 144.051, "note": "RB3"},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 200.07, "note": "RB3"},
|
||||
{"width": 2560, "height": 1440, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 2560, "height": 1440, "refresh_rate": 60.0, "note": "RB2"},
|
||||
{"width": 2560, "height": 1440, "refresh_rate": 144.05, "note": "RB3"},
|
||||
{"width": 2560, "height": 1440, "refresh_rate": 200.07, "note": "RB3"},
|
||||
{"width": 2560, "height": 1920, "refresh_rate": 74.979},
|
||||
{"width": 2728, "height": 1536, "refresh_rate": 59.944},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 60.0},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 60.0, "note": "RB2"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 120.0},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 120.0, "note": "RB1"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 120.0, "note": "RB2"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 165.0, "note": "RB1"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 165.0, "note": "RB2"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 200.0, "note": "RB1"},
|
||||
{"width": 3440, "height": 1440, "refresh_rate": 200.0, "note": "RB2"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 30.0, "note": "RB1"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 30.0, "note": "RB2"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 60.0, "note": "RB2"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 60.021, "note": "RB3"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 120.0, "note": "RB1"},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 120.0, "note": "RB2"},
|
||||
{"width": 4096, "height": 2160, "refresh_rate": 60.0, "note": "RB1"},
|
||||
{"width": 4096, "height": 2160, "refresh_rate": 60.0, "note": "RB2"},
|
||||
{"width": 4096, "height": 2160, "refresh_rate": 60.021, "note": "RB3"},
|
||||
]
|
||||
}
|
||||
|
||||
# OVT分辨率ID映射
|
||||
ovt_resolution_map = {
|
||||
0: [
|
||||
{"width": 768, "height": 480, "refresh_rate": 85.0},
|
||||
{"width": 1024, "height": 640, "refresh_rate": 60.0},
|
||||
{"width": 1152, "height": 720, "refresh_rate": 75.0},
|
||||
{"width": 1280, "height": 768, "refresh_rate": 60.0},
|
||||
{"width": 1280, "height": 960, "refresh_rate": 60.0},
|
||||
{"width": 1440, "height": 240, "refresh_rate": 60.0},
|
||||
{"width": 1440, "height": 480, "refresh_rate": 60.0},
|
||||
{"width": 1440, "height": 900, "refresh_rate": 60.0},
|
||||
{"width": 1536, "height": 960, "refresh_rate": 85.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 30.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 60.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 85.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 100.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 120.0},
|
||||
{"width": 1920, "height": 1440, "refresh_rate": 60.0},
|
||||
{"width": 2048, "height": 1280, "refresh_rate": 60.0},
|
||||
{"width": 2048, "height": 1536, "refresh_rate": 60.0},
|
||||
{"width": 2128, "height": 1200, "refresh_rate": 60.0},
|
||||
{"width": 2456, "height": 1536, "refresh_rate": 50.0},
|
||||
{"width": 2456, "height": 1536, "refresh_rate": 75.0},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 30.0},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 60.0},
|
||||
{"width": 2560, "height": 1080, "refresh_rate": 120.0},
|
||||
{"width": 2560, "height": 1600, "refresh_rate": 60.0},
|
||||
{"width": 2560, "height": 1920, "refresh_rate": 75.0},
|
||||
{"width": 2728, "height": 1536, "refresh_rate": 60.0},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 30.0},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 60.0},
|
||||
{"width": 3840, "height": 2160, "refresh_rate": 120.0},
|
||||
{"width": 4096, "height": 2160, "refresh_rate": 30.0},
|
||||
],
|
||||
1: [
|
||||
{"width": 1280, "height": 720, "refresh_rate": 24.0},
|
||||
{"width": 1280, "height": 720, "refresh_rate": 120.0},
|
||||
],
|
||||
4: [
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 144.0},
|
||||
{"width": 1920, "height": 1080, "refresh_rate": 240.0},
|
||||
],
|
||||
}
|
||||
|
||||
# 根据分辨率类型和ID获取分辨率信息的函数
|
||||
@staticmethod
|
||||
def get_resolution_info(resolution_type, resolution_id):
|
||||
"""
|
||||
根据分辨率类型和ID获取分辨率信息
|
||||
|
||||
Args:
|
||||
resolution_type: 分辨率类型,可以是DMT、CTA、CVT或OVT
|
||||
resolution_id: 分辨率ID
|
||||
|
||||
Returns:
|
||||
包含分辨率信息的字典,如果未找到则返回None
|
||||
"""
|
||||
if resolution_type == UCDEnum.ResolutionType.DMT:
|
||||
return UCDEnum.dmt_resolution_map.get(resolution_id)
|
||||
elif resolution_type == UCDEnum.ResolutionType.CTA:
|
||||
return UCDEnum.cta_resolution_map.get(resolution_id)
|
||||
elif resolution_type == UCDEnum.ResolutionType.CVT:
|
||||
resolutions = UCDEnum.cvt_resolution_map.get(resolution_id, [])
|
||||
return resolutions[0] if resolutions else None
|
||||
elif resolution_type == UCDEnum.ResolutionType.OVT:
|
||||
resolutions = UCDEnum.ovt_resolution_map.get(resolution_id, [])
|
||||
return resolutions[0] if resolutions else None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_formatted_resolution_list():
|
||||
"""
|
||||
从分辨率映射中生成格式化的分辨率字符串列表,用于UI显示
|
||||
格式为: "类型 宽度x 高度 @ 刷新率Hz"
|
||||
|
||||
Returns:
|
||||
包含格式化分辨率字符串的列表
|
||||
"""
|
||||
formatted_list = []
|
||||
|
||||
# 添加DMT分辨率
|
||||
for res_id, res_info in UCDEnum.TimingInfo.dmt_resolution_map.items():
|
||||
formatted_str = f"DMT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
|
||||
formatted_list.append(formatted_str)
|
||||
|
||||
# 添加CTA分辨率
|
||||
for res_id, res_info in UCDEnum.TimingInfo.cta_resolution_map.items():
|
||||
formatted_str = f"CTA {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
|
||||
formatted_list.append(formatted_str)
|
||||
|
||||
# 添加CVT分辨率 (只取每个ID的第一个)
|
||||
for res_id, res_list in UCDEnum.TimingInfo.cvt_resolution_map.items():
|
||||
for res_info in res_list:
|
||||
note = f" {res_info.get('note', '')}" if "note" in res_info else ""
|
||||
formatted_str = f"CVT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz{note}"
|
||||
formatted_list.append(formatted_str)
|
||||
|
||||
# 添加OVT分辨率 (只取每个ID的第一个)
|
||||
for res_id, res_list in UCDEnum.TimingInfo.ovt_resolution_map.items():
|
||||
for res_info in res_list:
|
||||
formatted_str = f"OVT {res_info['width']}x {res_info['height']} @ {int(res_info['refresh_rate'])}Hz"
|
||||
formatted_list.append(formatted_str)
|
||||
|
||||
# 排序并去重
|
||||
return sorted(list(set(formatted_list)))
|
||||
|
||||
class SignalFormat:
|
||||
"""信号格式相关枚举"""
|
||||
|
||||
class GammaType:
|
||||
"""Gamma 类型枚举"""
|
||||
|
||||
GAMMA_22 = "2.2"
|
||||
GAMMA_24 = "2.4"
|
||||
GAMMA_26 = "2.6"
|
||||
|
||||
@staticmethod
|
||||
def get_list():
|
||||
return ["2.2", "2.4", "2.6"]
|
||||
|
||||
@staticmethod
|
||||
def get_gamma_value(gamma_str):
|
||||
"""将 Gamma 字符串转换为数值"""
|
||||
gamma_map = {"2.2": 2.2, "2.4": 2.4, "2.6": 2.6}
|
||||
return gamma_map.get(gamma_str, 2.2)
|
||||
|
||||
class DataRange:
|
||||
"""数据范围枚举"""
|
||||
|
||||
FULL = "Full"
|
||||
LIMITED = "Limited"
|
||||
|
||||
@staticmethod
|
||||
def get_list():
|
||||
return ["Full", "Limited"]
|
||||
|
||||
class BitDepth:
|
||||
"""编码位深枚举"""
|
||||
|
||||
BIT_8 = "8bit"
|
||||
BIT_10 = "10bit"
|
||||
BIT_12 = "12bit"
|
||||
|
||||
@staticmethod
|
||||
def get_list():
|
||||
return ["8bit", "10bit", "12bit"]
|
||||
|
||||
@staticmethod
|
||||
def get_bit_value(bit_str):
|
||||
"""将位深字符串转换为数值"""
|
||||
bit_map = {"8bit": 8, "10bit": 10, "12bit": 12}
|
||||
return bit_map.get(bit_str, 8)
|
||||
|
||||
class HDRMetadata:
|
||||
"""HDR Metadata 参数"""
|
||||
|
||||
# MaxCLL (Maximum Content Light Level) - 最大内容亮度级别
|
||||
MAX_CLL_DEFAULT = 1000
|
||||
MAX_CLL_OPTIONS = [400, 600, 800, 1000, 1200, 1500, 2000, 4000]
|
||||
|
||||
# MaxFALL (Maximum Frame Average Light Level) - 最大帧平均亮度级别
|
||||
MAX_FALL_DEFAULT = 400
|
||||
MAX_FALL_OPTIONS = [200, 300, 400, 500, 600, 800, 1000]
|
||||
|
||||
@staticmethod
|
||||
def get_maxcll_list():
|
||||
return [400, 600, 800, 1000, 1200, 1500, 2000, 4000]
|
||||
|
||||
@staticmethod
|
||||
def get_maxfall_list():
|
||||
return [200, 300, 400, 500, 600, 800, 1000]
|
||||
|
||||
class OutputFormat:
|
||||
"""输出色彩格式枚举(决定信号是 RGB 还是 YCbCr 格式)"""
|
||||
|
||||
RGB = "RGB"
|
||||
YCBCR_422 = "YCbCr 4:2:2"
|
||||
YCBCR_444 = "YCbCr 4:4:4"
|
||||
YCBCR_420 = "YCbCr 4:2:0"
|
||||
Y_ONLY = "Y Only"
|
||||
IDO_DEFINED = "IDO Defined"
|
||||
RAW = "RAW"
|
||||
DSC = "DSC"
|
||||
|
||||
@staticmethod
|
||||
def get_list():
|
||||
return ["RGB", "YCbCr 4:4:4", "YCbCr 4:2:2", "YCbCr 4:2:0",
|
||||
"Y Only", "IDO Defined", "RAW", "DSC"]
|
||||
|
||||
@staticmethod
|
||||
def is_ycbcr(format_str):
|
||||
return "YCbCr" in (format_str or "")
|
||||
|
||||
@staticmethod
|
||||
def get_format_key(format_str):
|
||||
"""将显示字符串转换为 UCDEnum.ColorInfo.get_color_format() 的 key"""
|
||||
fmt_map = {
|
||||
"RGB": "rgb",
|
||||
"YCbCr 4:4:4": "ycbcr444",
|
||||
"YCbCr 4:2:2": "ycbcr422",
|
||||
"YCbCr 4:2:0": "ycbcr420",
|
||||
"Y Only": "yonly",
|
||||
"IDO Defined": "ido_defined",
|
||||
"RAW": "raw",
|
||||
"DSC": "dsc",
|
||||
}
|
||||
return fmt_map.get(format_str, "rgb")
|
||||
|
||||
412
app/ucd/service.py
Normal file
412
app/ucd/service.py
Normal file
@@ -0,0 +1,412 @@
|
||||
"""UCD 服务层:SignalService(硬件编排)+ PatternService(测试发图)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from app.ucd.domain import (
|
||||
EventBus,
|
||||
PatternSpec,
|
||||
SignalFormat,
|
||||
TimingSpec,
|
||||
UcdState,
|
||||
image_pattern,
|
||||
solid_rgb_pattern,
|
||||
)
|
||||
from app.ucd.device import IUcdDevice
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─── SignalService ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class SignalService:
|
||||
"""协调 SignalFormat / Timing / Pattern 的写入与提交。"""
|
||||
|
||||
def __init__(self, device: IUcdDevice, bus: EventBus):
|
||||
self._dev = device
|
||||
self._bus = bus
|
||||
|
||||
# -- 高层接口 ------------------------------------------------
|
||||
|
||||
def apply(
|
||||
self,
|
||||
*,
|
||||
signal: SignalFormat,
|
||||
timing: TimingSpec,
|
||||
pattern: PatternSpec,
|
||||
) -> bool:
|
||||
"""一次性提交信号格式 + timing + 图案。
|
||||
|
||||
Returns:
|
||||
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
|
||||
"""
|
||||
log.info(
|
||||
"SignalService.apply signal=%s timing=%s pattern=%s",
|
||||
signal,
|
||||
timing,
|
||||
pattern.kind.value,
|
||||
)
|
||||
changed = self._dev.configure(signal, timing)
|
||||
self._dev.set_pattern(pattern)
|
||||
self._dev.apply()
|
||||
return changed
|
||||
|
||||
def send_pattern(self, pattern: PatternSpec) -> None:
|
||||
"""在已 configure 的信号上仅更新图案后 apply。"""
|
||||
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
|
||||
self._dev.set_pattern(pattern)
|
||||
self._dev.apply()
|
||||
|
||||
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
|
||||
self.send_pattern(solid_rgb_pattern(rgb))
|
||||
|
||||
def send_image(self, path: str) -> None:
|
||||
self.send_pattern(image_pattern(path))
|
||||
|
||||
def update_signal_format(
|
||||
self,
|
||||
*,
|
||||
color_space: str,
|
||||
output_format: str,
|
||||
bit_depth: str,
|
||||
data_range: str = "Full",
|
||||
max_cll: int | None = None,
|
||||
max_fall: int | None = None,
|
||||
) -> bool:
|
||||
"""仅将信号格式提交到 SDK(沿用上一次的 timing),不切换图案。
|
||||
|
||||
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
|
||||
"""
|
||||
_ = build_signal_format(
|
||||
color_space=color_space,
|
||||
output_format=output_format,
|
||||
bit_depth=bit_depth,
|
||||
data_range=data_range,
|
||||
)
|
||||
return self._dev.apply_signal_format(
|
||||
color_space=color_space,
|
||||
color_format=output_format,
|
||||
bit_depth=bit_depth,
|
||||
data_range=data_range,
|
||||
max_cll=max_cll,
|
||||
max_fall=max_fall,
|
||||
)
|
||||
|
||||
# -- 透传给上层的查询 ---------------------------------------
|
||||
|
||||
@property
|
||||
def device(self) -> IUcdDevice:
|
||||
return self._dev
|
||||
|
||||
def current_resolution(self) -> tuple[int, int]:
|
||||
return self._dev.current_resolution()
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""UCD 设备是否已打开。供 GUI 做前置校验。"""
|
||||
return self._dev.state != UcdState.CLOSED
|
||||
|
||||
@property
|
||||
def format_changed(self) -> bool:
|
||||
"""最近一次视频模式提交是否相对上次发生变化。"""
|
||||
return self._dev.format_changed
|
||||
|
||||
@property
|
||||
def last_error(self) -> str | None:
|
||||
return self._dev.last_error
|
||||
|
||||
def apply_config(self, config) -> bool:
|
||||
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern(不 apply 输出)。"""
|
||||
return bool(self._dev.set_ucd_params(config))
|
||||
|
||||
def send_pattern_params(self, params) -> bool:
|
||||
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
|
||||
return bool(self._dev.send_current_pattern_params(params))
|
||||
|
||||
def apply_and_run(self, config, pattern_params) -> bool:
|
||||
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
|
||||
return bool(self._dev.apply_config_and_run(config, pattern_params))
|
||||
|
||||
def stage_test_profile(
|
||||
self,
|
||||
config,
|
||||
*,
|
||||
color_space: str,
|
||||
output_format: str,
|
||||
bit_depth: str,
|
||||
data_range: str = "Full",
|
||||
max_cll: int | None = None,
|
||||
max_fall: int | None = None,
|
||||
) -> bool:
|
||||
"""按 PQConfig stage 色彩/Timing/Pattern 类型,并提交 UI 覆盖的信号格式。
|
||||
|
||||
自动化测试在发图前调用;等价于 ``apply_config`` + ``update_signal_format``。
|
||||
"""
|
||||
if not self.apply_config(config):
|
||||
return False
|
||||
return self.update_signal_format(
|
||||
color_space=color_space,
|
||||
output_format=output_format,
|
||||
bit_depth=bit_depth,
|
||||
data_range=data_range,
|
||||
max_cll=max_cll,
|
||||
max_fall=max_fall,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"SignalService",
|
||||
"build_signal_format",
|
||||
"build_signal_format_from_profile",
|
||||
"build_timing",
|
||||
"solid_rgb_pattern",
|
||||
"image_pattern",
|
||||
"SignalFormat",
|
||||
"TimingSpec",
|
||||
"PatternSpec",
|
||||
"PatternKind",
|
||||
"Colorimetry",
|
||||
"DynamicRange",
|
||||
"UcdError",
|
||||
]
|
||||
|
||||
|
||||
# --- PatternService ---
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.data_range_converter import convert_pattern_params
|
||||
from app.pq.pq_config import get_pattern
|
||||
|
||||
|
||||
@dataclass
|
||||
class PatternSession:
|
||||
mode: str
|
||||
test_type: str
|
||||
active_config: object
|
||||
pattern_params: list[list[int]]
|
||||
total_patterns: int
|
||||
display_names: list[str]
|
||||
|
||||
|
||||
class PatternService:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def _build_apply_config_error(self, test_type):
|
||||
timing = self.app.config.current_test_types.get(test_type, {}).get("timing", "-")
|
||||
detail = ""
|
||||
err = self.app.signal_service.last_error
|
||||
if err:
|
||||
detail = f", detail={err}"
|
||||
return f"UCD profile apply_config failed for {test_type}, timing={timing}{detail}"
|
||||
|
||||
def _stage_profile(
|
||||
self,
|
||||
active_config,
|
||||
*,
|
||||
color_space: str,
|
||||
output_format: str,
|
||||
bit_depth: str,
|
||||
data_range: str = "Full",
|
||||
max_cll: int | None = None,
|
||||
max_fall: int | None = None,
|
||||
test_type: str,
|
||||
log_details: bool = False,
|
||||
log_title: str = "",
|
||||
log_fields: list[tuple[str, str]] | None = None,
|
||||
) -> None:
|
||||
if log_details and log_title:
|
||||
self._log("=" * 50, "separator")
|
||||
self._log(log_title, "info")
|
||||
self._log("=" * 50, "separator")
|
||||
for label, value in log_fields or []:
|
||||
self._log(f" {label}: {value}", "info")
|
||||
|
||||
if not self.app.signal_service.stage_test_profile(
|
||||
active_config,
|
||||
color_space=color_space,
|
||||
output_format=output_format,
|
||||
bit_depth=bit_depth,
|
||||
data_range=data_range,
|
||||
max_cll=max_cll,
|
||||
max_fall=max_fall,
|
||||
):
|
||||
raise RuntimeError(self._build_apply_config_error(test_type))
|
||||
|
||||
if log_details:
|
||||
self._log("信号格式设置成功", "success")
|
||||
|
||||
def prepare_session(self, mode, *, test_type=None, log_details=False):
|
||||
test_type = test_type or self.app.config.current_test_type
|
||||
if hasattr(self.app.config, "set_current_test_type"):
|
||||
self.app.config.set_current_test_type(test_type)
|
||||
if not self.app.config.set_current_pattern(mode):
|
||||
raise ValueError(f"未知的图案模式: {mode}")
|
||||
|
||||
active_config = self.app.config
|
||||
source_params = self._get_source_pattern_params(mode)
|
||||
|
||||
if test_type == "screen_module":
|
||||
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
|
||||
color_space = (
|
||||
self.app.screen_module_color_space_var.get()
|
||||
if hasattr(self.app, "screen_module_color_space_var")
|
||||
else screen_cfg.get("colorimetry", "sRGB")
|
||||
)
|
||||
data_range = (
|
||||
self.app.screen_module_data_range_var.get()
|
||||
if hasattr(self.app, "screen_module_data_range_var")
|
||||
else screen_cfg.get("data_range", "Full")
|
||||
)
|
||||
bit_depth = (
|
||||
self.app.screen_module_bit_depth_var.get()
|
||||
if hasattr(self.app, "screen_module_bit_depth_var")
|
||||
else f"{int(screen_cfg.get('bpc', 8))}bit"
|
||||
)
|
||||
output_format = (
|
||||
self.app.screen_module_output_format_var.get()
|
||||
if hasattr(self.app, "screen_module_output_format_var")
|
||||
else screen_cfg.get("color_format", "RGB")
|
||||
)
|
||||
|
||||
self._stage_profile(
|
||||
active_config,
|
||||
color_space=color_space,
|
||||
data_range=data_range,
|
||||
bit_depth=bit_depth,
|
||||
output_format=output_format,
|
||||
test_type=test_type,
|
||||
log_details=log_details,
|
||||
log_title="设置屏模组信号格式:",
|
||||
log_fields=[
|
||||
("色彩空间", color_space),
|
||||
("色彩格式", output_format),
|
||||
("数据范围", data_range),
|
||||
("编码位深", bit_depth),
|
||||
("Timing", self.app.config.current_test_types[test_type]["timing"]),
|
||||
],
|
||||
)
|
||||
|
||||
elif test_type == "sdr_movie":
|
||||
data_range = self.app.sdr_data_range_var.get()
|
||||
converted_params = convert_pattern_params(
|
||||
source_params, data_range=data_range, verbose=False
|
||||
)
|
||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||
mode=mode, converted_params=converted_params
|
||||
)
|
||||
if hasattr(active_config, "set_current_test_type"):
|
||||
active_config.set_current_test_type(test_type)
|
||||
|
||||
self._stage_profile(
|
||||
active_config,
|
||||
color_space=self.app.sdr_color_space_var.get(),
|
||||
data_range=data_range,
|
||||
bit_depth=self.app.sdr_bit_depth_var.get(),
|
||||
output_format=self.app.sdr_output_format_var.get(),
|
||||
test_type=test_type,
|
||||
log_details=log_details,
|
||||
log_title="设置 SDR 信号格式:",
|
||||
log_fields=[
|
||||
("色彩空间", self.app.sdr_color_space_var.get()),
|
||||
("色彩格式", self.app.sdr_output_format_var.get()),
|
||||
("Gamma", self.app.sdr_gamma_type_var.get()),
|
||||
("数据范围", data_range),
|
||||
("编码位深", self.app.sdr_bit_depth_var.get()),
|
||||
],
|
||||
)
|
||||
if log_details:
|
||||
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
||||
|
||||
elif test_type == "hdr_movie":
|
||||
data_range = self.app.hdr_data_range_var.get()
|
||||
converted_params = convert_pattern_params(
|
||||
source_params, data_range=data_range, verbose=False
|
||||
)
|
||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||
mode=mode, converted_params=converted_params
|
||||
)
|
||||
if hasattr(active_config, "set_current_test_type"):
|
||||
active_config.set_current_test_type(test_type)
|
||||
|
||||
self._stage_profile(
|
||||
active_config,
|
||||
color_space=self.app.hdr_color_space_var.get(),
|
||||
data_range=data_range,
|
||||
bit_depth=self.app.hdr_bit_depth_var.get(),
|
||||
output_format=self.app.hdr_output_format_var.get(),
|
||||
max_cll=self.app.hdr_maxcll_var.get(),
|
||||
max_fall=self.app.hdr_maxfall_var.get(),
|
||||
test_type=test_type,
|
||||
log_details=log_details,
|
||||
log_title="设置 HDR 信号格式:",
|
||||
log_fields=[
|
||||
("色彩空间", self.app.hdr_color_space_var.get()),
|
||||
("色彩格式", self.app.hdr_output_format_var.get()),
|
||||
("数据范围", data_range),
|
||||
("编码位深", self.app.hdr_bit_depth_var.get()),
|
||||
("MaxCLL", self.app.hdr_maxcll_var.get()),
|
||||
("MaxFALL", self.app.hdr_maxfall_var.get()),
|
||||
],
|
||||
)
|
||||
if log_details:
|
||||
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
||||
|
||||
else:
|
||||
raise ValueError(f"不支持的测试类型: {test_type}")
|
||||
|
||||
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
|
||||
return PatternSession(
|
||||
mode=mode,
|
||||
test_type=test_type,
|
||||
active_config=active_config,
|
||||
pattern_params=pattern_params,
|
||||
total_patterns=len(pattern_params),
|
||||
display_names=self._get_display_names(mode, len(pattern_params)),
|
||||
)
|
||||
|
||||
def send_session_pattern(self, session, index):
|
||||
if index < 0 or index >= session.total_patterns:
|
||||
raise IndexError(f"pattern 索引越界: {index}")
|
||||
|
||||
pattern_param = session.pattern_params[index]
|
||||
if not self.app.signal_service.send_pattern_params(pattern_param):
|
||||
raise RuntimeError(f"发送 pattern 失败: {index}")
|
||||
return pattern_param
|
||||
|
||||
def send_rgb(self, rgb, *, session=None, test_type=None):
|
||||
active_session = session or self.prepare_session(
|
||||
"rgb",
|
||||
test_type=test_type,
|
||||
log_details=False,
|
||||
)
|
||||
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
|
||||
self.app.signal_service.send_solid_rgb(converted_rgb)
|
||||
return True
|
||||
|
||||
def _get_source_pattern_params(self, mode):
|
||||
return copy.deepcopy(get_pattern(mode)["pattern_params"])
|
||||
|
||||
def _get_display_names(self, mode, total_patterns):
|
||||
if mode == "accuracy":
|
||||
return self.app.config.get_accuracy_color_names()
|
||||
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
|
||||
return self.app.config.get_temp_pattern_names()
|
||||
return [f"P {index + 1}" for index in range(total_patterns)]
|
||||
|
||||
def _convert_rgb_for_test_type(self, rgb, test_type):
|
||||
if test_type == "sdr_movie":
|
||||
data_range = self.app.sdr_data_range_var.get()
|
||||
elif test_type == "hdr_movie":
|
||||
data_range = self.app.hdr_data_range_var.get()
|
||||
else:
|
||||
data_range = "Full"
|
||||
|
||||
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
|
||||
|
||||
def _log(self, message, level):
|
||||
if hasattr(self.app, "log_gui"):
|
||||
self.app.log_gui.log(message, level=level)
|
||||
Reference in New Issue
Block a user