1.1.0版本
This commit is contained in:
590
utils/UCD323_Enum.py
Normal file
590
utils/UCD323_Enum.py
Normal file
@@ -0,0 +1,590 @@
|
||||
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
|
||||
return 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 [
|
||||
SignalFormat.GammaType.GAMMA_22,
|
||||
SignalFormat.GammaType.GAMMA_24,
|
||||
SignalFormat.GammaType.GAMMA_26,
|
||||
]
|
||||
|
||||
@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 [SignalFormat.DataRange.FULL, SignalFormat.DataRange.LIMITED]
|
||||
|
||||
@staticmethod
|
||||
def is_full_range(range_str):
|
||||
"""判断是否为 Full Range"""
|
||||
return range_str == SignalFormat.DataRange.FULL
|
||||
|
||||
class BitDepth:
|
||||
"""编码位深枚举"""
|
||||
|
||||
BIT_8 = "8bit"
|
||||
BIT_10 = "10bit"
|
||||
BIT_12 = "12bit"
|
||||
|
||||
@staticmethod
|
||||
def get_list():
|
||||
return [
|
||||
SignalFormat.BitDepth.BIT_8,
|
||||
SignalFormat.BitDepth.BIT_10,
|
||||
SignalFormat.BitDepth.BIT_12,
|
||||
]
|
||||
|
||||
@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 SignalFormat.HDRMetadata.MAX_CLL_OPTIONS
|
||||
|
||||
@staticmethod
|
||||
def get_maxfall_list():
|
||||
return SignalFormat.HDRMetadata.MAX_FALL_OPTIONS
|
||||
589
utils/UCD323_Function.py
Normal file
589
utils/UCD323_Function.py
Normal file
@@ -0,0 +1,589 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
import UniTAP
|
||||
import time
|
||||
import gc
|
||||
from utils.UCD323_Enum import UCDEnum
|
||||
|
||||
|
||||
class UCDController:
|
||||
"""UCD323信号发生器控制类"""
|
||||
|
||||
def __init__(self):
|
||||
self.lUniTAP = UniTAP.TsiLib()
|
||||
self.dev = None
|
||||
self.role = None
|
||||
self.timing_manager = None
|
||||
self.config = None
|
||||
self.color_mode = None
|
||||
self.status = False
|
||||
|
||||
self.current_timing = None
|
||||
self.current_pattern = None
|
||||
self.current_pattern_param = None
|
||||
self.current_pattern_params = None
|
||||
self.current_pattern_index = 0
|
||||
|
||||
def search_device(self):
|
||||
"""搜索可用设备"""
|
||||
available_devices = self.lUniTAP.get_list_of_available_devices()
|
||||
return available_devices if available_devices else []
|
||||
|
||||
def open(self, device_name):
|
||||
"""打开设备"""
|
||||
temp_dev = None
|
||||
|
||||
try:
|
||||
if self.dev is not None or self.status:
|
||||
self._force_cleanup()
|
||||
|
||||
device_id = int(device_name.split(":")[0])
|
||||
temp_dev = self.lUniTAP.open(device_id)
|
||||
|
||||
try:
|
||||
self.role = temp_dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
||||
self.dev = temp_dev
|
||||
|
||||
except Exception as role_error:
|
||||
self._close_device_object(temp_dev)
|
||||
raise role_error
|
||||
|
||||
self.timing_manager = self.role.hdtx.pg.timing_manager
|
||||
self.color_mode = UniTAP.ColorInfo()
|
||||
self.status = True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._force_cleanup()
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""关闭设备"""
|
||||
try:
|
||||
if self.dev:
|
||||
self._close_device_object(self.dev)
|
||||
|
||||
self.dev = None
|
||||
self.role = None
|
||||
self.status = False
|
||||
self.timing_manager = None
|
||||
self.current_timing = None
|
||||
self.current_pattern = None
|
||||
self.current_pattern_param = None
|
||||
self.current_pattern_params = None
|
||||
self.current_pattern_index = 0
|
||||
|
||||
self.lUniTAP = None
|
||||
|
||||
for i in range(3):
|
||||
gc.collect()
|
||||
|
||||
time.sleep(2.0)
|
||||
|
||||
self.lUniTAP = UniTAP.TsiLib()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.dev = None
|
||||
self.role = None
|
||||
self.status = False
|
||||
self.timing_manager = None
|
||||
self.current_timing = None
|
||||
self.current_pattern = None
|
||||
self.current_pattern_param = None
|
||||
self.current_pattern_params = None
|
||||
self.current_pattern_index = 0
|
||||
|
||||
try:
|
||||
self.lUniTAP = None
|
||||
gc.collect()
|
||||
time.sleep(2.0)
|
||||
self.lUniTAP = UniTAP.TsiLib()
|
||||
except Exception as init_error:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def _close_device_object(self, dev_obj):
|
||||
"""显式关闭设备对象"""
|
||||
try:
|
||||
if dev_obj is None:
|
||||
return
|
||||
|
||||
if self.lUniTAP and hasattr(self.lUniTAP, "close"):
|
||||
try:
|
||||
self.lUniTAP.close(dev_obj)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
dev_obj = None
|
||||
gc.collect()
|
||||
time.sleep(1.0)
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _force_cleanup(self):
|
||||
"""强制清理所有状态"""
|
||||
try:
|
||||
if self.dev:
|
||||
self._close_device_object(self.dev)
|
||||
|
||||
self.dev = None
|
||||
self.role = None
|
||||
self.status = False
|
||||
self.timing_manager = None
|
||||
self.current_timing = None
|
||||
self.current_pattern = None
|
||||
self.current_pattern_param = None
|
||||
self.current_pattern_params = None
|
||||
self.current_pattern_index = 0
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _cleanup(self):
|
||||
"""清理设备资源"""
|
||||
try:
|
||||
if self.dev:
|
||||
self._close_device_object(self.dev)
|
||||
self.dev = None
|
||||
|
||||
if hasattr(self.lUniTAP, "cleanup"):
|
||||
self.lUniTAP.cleanup()
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def set_ucd_params(self, config):
|
||||
"""设置UCD323参数"""
|
||||
self.config = config
|
||||
test_type = self.config.current_test_type
|
||||
|
||||
if test_type == "hdr_movie":
|
||||
color_format_str = self.config.current_test_types[test_type]["color_format"]
|
||||
color_format = UCDEnum.ColorInfo.get_color_format(color_format_str)
|
||||
|
||||
if color_format:
|
||||
self.color_mode.color_format = color_format
|
||||
else:
|
||||
return False
|
||||
|
||||
else:
|
||||
color_format = self.config.current_test_types[test_type]["color_format"]
|
||||
bpc = self.config.current_test_types[test_type]["bpc"]
|
||||
colorimetry = self.config.current_test_types[test_type]["colorimetry"]
|
||||
|
||||
if not self.set_color_mode(color_format, bpc, colorimetry):
|
||||
return False
|
||||
|
||||
timing_str = self.config.current_test_types[test_type]["timing"]
|
||||
self.set_timing_from_string(timing_str)
|
||||
|
||||
self.current_pattern_index = 0
|
||||
pattern_mode = self.config.current_pattern["pattern_mode"]
|
||||
pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern_mode)
|
||||
|
||||
if pattern is None:
|
||||
return False
|
||||
|
||||
self.current_pattern = pattern
|
||||
self.current_pattern_params = self.config.current_pattern["pattern_params"]
|
||||
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
"""运行设备"""
|
||||
self.apply_video_mode()
|
||||
self.apply_pattern()
|
||||
self.role.hdtx.pg.apply()
|
||||
return True
|
||||
|
||||
def set_color_mode(self, cf, bpc, cm):
|
||||
"""设置颜色模式"""
|
||||
current_dynamic_range = None
|
||||
current_transfer_characteristic = None
|
||||
|
||||
if hasattr(self.color_mode, "dynamic_range"):
|
||||
current_dynamic_range = self.color_mode.dynamic_range
|
||||
|
||||
if hasattr(self.color_mode, "transfer_characteristic"):
|
||||
current_transfer_characteristic = self.color_mode.transfer_characteristic
|
||||
|
||||
color_format = UCDEnum.ColorInfo.get_color_format(cf)
|
||||
if color_format is None:
|
||||
return False
|
||||
|
||||
if not isinstance(bpc, int) or bpc <= 0:
|
||||
return False
|
||||
|
||||
colorimetry = UCDEnum.ColorInfo.get_colorimetry(cm)
|
||||
if colorimetry is None:
|
||||
return False
|
||||
|
||||
self.color_mode.color_format = color_format
|
||||
self.color_mode.bpc = bpc
|
||||
self.color_mode.colorimetry = colorimetry
|
||||
|
||||
if current_dynamic_range is not None:
|
||||
self.color_mode.dynamic_range = current_dynamic_range
|
||||
|
||||
if current_transfer_characteristic is not None:
|
||||
self.color_mode.transfer_characteristic = current_transfer_characteristic
|
||||
|
||||
return True
|
||||
|
||||
def apply_video_mode(self):
|
||||
"""应用当前colormode和timing"""
|
||||
if self.current_timing:
|
||||
self.set_video_mode()
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_video_mode(self):
|
||||
"""设置视频模式"""
|
||||
video_mode = UniTAP.VideoMode(
|
||||
timing=self.current_timing, color_info=self.color_mode
|
||||
)
|
||||
self.role.hdtx.pg.set_vm(vm=video_mode)
|
||||
return True
|
||||
|
||||
def set_pattern(self, pattern, pattern_params=None):
|
||||
"""设置pattern"""
|
||||
if self.current_timing:
|
||||
if (
|
||||
pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor
|
||||
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips
|
||||
or pattern
|
||||
== UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes
|
||||
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern
|
||||
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow
|
||||
and pattern_params
|
||||
):
|
||||
self.set_pattern_params(pattern, pattern_params)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_next_pattern(self):
|
||||
"""设置下一个pattern"""
|
||||
if self.current_pattern_index < len(self.current_pattern_params):
|
||||
p = self.current_pattern_params[self.current_pattern_index]
|
||||
self.set_pattern(self.current_pattern, p)
|
||||
self.current_pattern_index += 1
|
||||
else:
|
||||
error_msg = (
|
||||
f"No more patterns to set. (已设置 {self.current_pattern_index} 个图案)"
|
||||
)
|
||||
raise IndexError(error_msg)
|
||||
|
||||
def set_pattern_params(self, pattern, pattern_params):
|
||||
"""设置pattern参数"""
|
||||
if pattern:
|
||||
if pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor:
|
||||
self.current_pattern_param = UniTAP.SolidColorParams(
|
||||
first=pattern_params[0],
|
||||
second=pattern_params[1],
|
||||
third=pattern_params[2],
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def apply_pattern(self):
|
||||
"""应用当前pattern"""
|
||||
if self.current_pattern:
|
||||
self.role.hdtx.pg.set_pattern(self.current_pattern)
|
||||
|
||||
if self.current_pattern_param:
|
||||
self.role.hdtx.pg.set_pattern_params(self.current_pattern_param)
|
||||
return True
|
||||
return False
|
||||
|
||||
def search_timing(self, width, height, refresh_rate, resolution_type=None):
|
||||
"""根据分辨率参数搜索合适的timing"""
|
||||
if resolution_type:
|
||||
resolution_type = resolution_type.lower()
|
||||
standard = None
|
||||
if resolution_type == "dmt":
|
||||
standard = UniTAP.common.timing.Timing.Standard.SD_DMT
|
||||
elif resolution_type == "cta":
|
||||
standard = UniTAP.common.timing.Timing.Standard.SD_CTA
|
||||
elif resolution_type == "cvt":
|
||||
standard = UniTAP.common.timing.Timing.Standard.SD_CVT
|
||||
|
||||
timing = self.timing_manager.search(
|
||||
h_active=width,
|
||||
v_active=height,
|
||||
f_rate=int(refresh_rate) * 1000,
|
||||
standard=standard,
|
||||
)
|
||||
|
||||
if timing:
|
||||
return timing
|
||||
else:
|
||||
for res_type in ["dmt", "cta", "cvt", "ovt"]:
|
||||
result = self.search_timing(width, height, refresh_rate, res_type)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
def parse_formatted_timing(self, timing_str):
|
||||
"""解析格式化的timing字符串"""
|
||||
if not isinstance(timing_str, str):
|
||||
raise ValueError("timing_str 必须是字符串")
|
||||
|
||||
s = " ".join(timing_str.strip().split())
|
||||
s = s.replace(" x", "x").replace("x ", "x")
|
||||
|
||||
parts = s.split(" ", 1)
|
||||
if len(parts) < 2:
|
||||
raise ValueError(f"无法解析timing: {timing_str}")
|
||||
type_str = parts[0].strip().upper()
|
||||
rest = parts[1].strip()
|
||||
|
||||
if "@" not in rest:
|
||||
raise ValueError(f"无法解析timing(缺少 @): {timing_str}")
|
||||
left, right = [p.strip() for p in rest.split("@", 1)]
|
||||
|
||||
if "x" not in left:
|
||||
raise ValueError(f"无法解析分辨率(缺少 x): {timing_str}")
|
||||
wh = left.split("x")
|
||||
if len(wh) != 2:
|
||||
raise ValueError(f"无法解析分辨率: {timing_str}")
|
||||
try:
|
||||
width = int(wh[0])
|
||||
height = int(wh[1])
|
||||
except Exception:
|
||||
raise ValueError(f"分辨率数字解析失败: {timing_str}")
|
||||
|
||||
hz_str = right.replace("Hz", "").replace("HZ", "").strip()
|
||||
try:
|
||||
refresh_rate = float(hz_str)
|
||||
except Exception:
|
||||
raise ValueError(f"刷新率解析失败: {timing_str}")
|
||||
|
||||
rtype_map = {
|
||||
"DMT": "dmt",
|
||||
"CTA": "cta",
|
||||
"CVT": "cvt",
|
||||
"OVT": "ovt",
|
||||
}
|
||||
if type_str not in rtype_map:
|
||||
raise ValueError(f"未知的分辨率类型: {type_str}")
|
||||
resolution_type = rtype_map[type_str]
|
||||
|
||||
def find_best_id_in_dict(res_map):
|
||||
best_id, best_diff = None, float("inf")
|
||||
for rid, info in res_map.items():
|
||||
if info["width"] == width and info["height"] == height:
|
||||
diff = abs(float(info["refresh_rate"]) - refresh_rate)
|
||||
if diff < best_diff:
|
||||
best_diff = diff
|
||||
best_id = rid
|
||||
return best_id if best_diff <= 1.0 else None
|
||||
|
||||
def find_best_id_in_list_map(res_map):
|
||||
best_id, best_diff = None, float("inf")
|
||||
for rid, infos in res_map.items():
|
||||
for info in infos:
|
||||
if info["width"] == width and info["height"] == height:
|
||||
diff = abs(float(info["refresh_rate"]) - refresh_rate)
|
||||
if diff < best_diff:
|
||||
best_diff = diff
|
||||
best_id = rid
|
||||
return best_id if best_diff <= 1.0 else None
|
||||
|
||||
resolution_id = None
|
||||
if resolution_type == "dmt":
|
||||
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.dmt_resolution_map)
|
||||
elif resolution_type == "cta":
|
||||
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.cta_resolution_map)
|
||||
elif resolution_type == "cvt":
|
||||
resolution_id = find_best_id_in_list_map(
|
||||
UCDEnum.TimingInfo.cvt_resolution_map
|
||||
)
|
||||
elif resolution_type == "ovt":
|
||||
resolution_id = find_best_id_in_list_map(
|
||||
UCDEnum.TimingInfo.ovt_resolution_map
|
||||
)
|
||||
|
||||
result = {
|
||||
"resolution_type": resolution_type,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"refresh_rate": refresh_rate,
|
||||
"resolution_id": resolution_id,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def set_timing_from_string(self, timing_str):
|
||||
"""根据格式化timing字符串设置设备timing"""
|
||||
spec = self.parse_formatted_timing(timing_str)
|
||||
rtype = spec["resolution_type"]
|
||||
width = spec["width"]
|
||||
height = spec["height"]
|
||||
fr = spec["refresh_rate"]
|
||||
|
||||
if rtype != "ovt":
|
||||
timing = self.search_timing(width, height, fr, rtype)
|
||||
if timing:
|
||||
self.current_timing = timing
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_timing_from_id(self, rtype, rid):
|
||||
"""根据(type, id)设置设备timing"""
|
||||
timing = None
|
||||
if rtype.lower() == "dmt":
|
||||
timing = self.timing_manager.get_dmt(rid)
|
||||
elif rtype.lower() == "cta":
|
||||
timing = self.timing_manager.get_cta(rid)
|
||||
elif rtype.lower() == "cvt":
|
||||
timing = self.timing_manager.get_cvt(rid)
|
||||
else:
|
||||
raise ValueError(f"不支持的分辨率类型: {rtype}")
|
||||
|
||||
if timing:
|
||||
self.current_timing = timing
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_sdr_format(
|
||||
self, color_space=None, gamma=None, data_range=None, bit_depth=None
|
||||
):
|
||||
"""设置SDR信号格式"""
|
||||
|
||||
def _get_colorimetry_from_color_space(color_space_name):
|
||||
"""将色彩空间名称转换为UniTAP colorimetry枚举值"""
|
||||
try:
|
||||
colorimetry_map = {
|
||||
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
|
||||
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
|
||||
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
|
||||
}
|
||||
result = colorimetry_map.get(color_space_name)
|
||||
return result if result else UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
|
||||
except Exception as e:
|
||||
return UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
|
||||
|
||||
def _set_gamma_transfer_characteristic(gamma_value_str):
|
||||
"""设置Gamma传输特性"""
|
||||
try:
|
||||
gamma_value = float(gamma_value_str)
|
||||
|
||||
if abs(gamma_value - 2.2) < 0.1:
|
||||
self.color_mode.transfer_characteristic = (
|
||||
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
|
||||
)
|
||||
return True
|
||||
|
||||
elif abs(gamma_value - 2.4) < 0.1:
|
||||
if hasattr(
|
||||
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA24"
|
||||
):
|
||||
self.color_mode.transfer_characteristic = (
|
||||
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA24
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self.color_mode.transfer_characteristic = (
|
||||
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
|
||||
)
|
||||
return False
|
||||
|
||||
elif abs(gamma_value - 2.6) < 0.1:
|
||||
if hasattr(
|
||||
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA26"
|
||||
):
|
||||
self.color_mode.transfer_characteristic = (
|
||||
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA26
|
||||
)
|
||||
return True
|
||||
else:
|
||||
self.color_mode.transfer_characteristic = (
|
||||
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
try:
|
||||
if color_space:
|
||||
colorimetry = _get_colorimetry_from_color_space(color_space)
|
||||
if colorimetry:
|
||||
self.color_mode.colorimetry = colorimetry
|
||||
|
||||
if gamma:
|
||||
_set_gamma_transfer_characteristic(gamma)
|
||||
|
||||
if data_range:
|
||||
if data_range == "Full":
|
||||
self.color_mode.dynamic_range = (
|
||||
UniTAP.ColorInfo.DynamicRange.DR_VESA
|
||||
)
|
||||
elif data_range == "Limited":
|
||||
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
|
||||
|
||||
if bit_depth:
|
||||
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
||||
self.color_mode.bpc = bpc
|
||||
|
||||
if self.current_timing:
|
||||
self.set_video_mode()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def set_hdr_format(
|
||||
self,
|
||||
color_space=None,
|
||||
data_range=None,
|
||||
bit_depth=None,
|
||||
max_cll=None,
|
||||
max_fall=None,
|
||||
):
|
||||
"""设置HDR信号格式"""
|
||||
try:
|
||||
if color_space:
|
||||
colorimetry = self._get_colorimetry_from_color_space(color_space)
|
||||
if colorimetry:
|
||||
self.color_mode.colorimetry = colorimetry
|
||||
|
||||
if data_range:
|
||||
if data_range == "Full":
|
||||
self.color_mode.dynamic_range = (
|
||||
UniTAP.ColorInfo.DynamicRange.DR_VESA
|
||||
)
|
||||
elif data_range == "Limited":
|
||||
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
|
||||
|
||||
if bit_depth:
|
||||
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
||||
self.color_mode.bpc = bpc
|
||||
|
||||
if self.current_timing:
|
||||
self.set_video_mode()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def _get_colorimetry_from_color_space(self, color_space):
|
||||
"""将色彩空间字符串转换为UniTAP.ColorInfo.Colorimetry"""
|
||||
colorimetry_map = {
|
||||
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
|
||||
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
|
||||
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
|
||||
"DCI-P3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
|
||||
}
|
||||
return colorimetry_map.get(color_space)
|
||||
117
utils/baseSerail.py
Normal file
117
utils/baseSerail.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
import serial
|
||||
import time
|
||||
import binascii
|
||||
|
||||
# CRC16矩阵表;
|
||||
CRC_TABLE = [
|
||||
0x0000, 0x1021, 0x2042, 0x3063,
|
||||
0x4084, 0x50A5, 0x60C6, 0x70E7,
|
||||
0x8108, 0x9129, 0xA14A, 0xB16B,
|
||||
0xC18C, 0xD1AD, 0xE1CE, 0xF1EF ]
|
||||
|
||||
|
||||
# 计算CRC16值;
|
||||
def crc16(bys, bysLen):
|
||||
uwCRC, ucTemp = 0xFFFF, 0x00
|
||||
index = 0
|
||||
while index < bysLen:
|
||||
ucTemp = (uwCRC >> 0x0C) & 0xFF
|
||||
uwCRC <<= 4
|
||||
uwCRC ^= CRC_TABLE[ucTemp ^ ((bys[index]) >> 0x04)]
|
||||
uwCRC &= 0xFFFF
|
||||
ucTemp = (uwCRC >> 0x0C) & 0xFF
|
||||
uwCRC <<= 4
|
||||
uwCRC ^= CRC_TABLE[ucTemp ^ (bys[index] & 0x0F)]
|
||||
uwCRC &= 0xFFFF
|
||||
index += 1
|
||||
return uwCRC
|
||||
|
||||
def convert_rgb16(rgb16_values):
|
||||
data = bytearray()
|
||||
for value in rgb16_values:
|
||||
high_byte = (value & 0xFF00) >> 8
|
||||
low_byte = value & 0x00FF
|
||||
|
||||
data.append(high_byte)
|
||||
data.append(low_byte)
|
||||
|
||||
return data
|
||||
|
||||
class BaseSerial():
|
||||
def __init__(self):
|
||||
self.exception = None
|
||||
self.ser = serial.Serial()
|
||||
|
||||
def open(self, port, baudrate=115200, bytesize=8, parity=serial.PARITY_NONE, stopbits=1, timeout=3,
|
||||
writeTimeout=3):
|
||||
if type(port) == type(0):
|
||||
self.ser.port = "COM" + str(port)
|
||||
else:
|
||||
self.ser.port = port
|
||||
# 波特率;
|
||||
self.ser.baudrate = baudrate
|
||||
# 数据位;
|
||||
self.ser.bytesize = bytesize
|
||||
# 校验码;
|
||||
self.ser.parity = parity # PARITY_NONE=0
|
||||
# 停止位;
|
||||
self.ser.stopbits = stopbits
|
||||
# 串口读超时值
|
||||
self.ser.timeout = timeout
|
||||
# 串口写超时值
|
||||
self.ser.write_timeout = writeTimeout
|
||||
try:
|
||||
self.ser.open()
|
||||
return self.ser.is_open
|
||||
except Exception as e:
|
||||
self.exception = e
|
||||
print('打开串口异常', str(e))
|
||||
return False
|
||||
|
||||
def reOpen(self):
|
||||
self.exception = None
|
||||
self.ser.close()
|
||||
try:
|
||||
self.ser.open()
|
||||
return self.ser.is_open
|
||||
except Exception as e:
|
||||
self.exception = e
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
self.ser.close()
|
||||
|
||||
def write(self, cmd: bytearray):
|
||||
try:
|
||||
self.exception = None
|
||||
writelen = self.ser.write(cmd)
|
||||
self.ser.flush()
|
||||
# print("writehex:" + str(binascii.b2a_hex(cmd)) + " writelen=" + str(writelen))
|
||||
return True if writelen == len(cmd) else False
|
||||
except Exception as e:
|
||||
self.exception = e
|
||||
print('写串口异常', str(e))
|
||||
return False
|
||||
|
||||
def read(self, size=0):
|
||||
try:
|
||||
self.exception = None
|
||||
bys = self.ser.read(1)
|
||||
for i in range(0, 3):
|
||||
while self.ser.in_waiting > 0:
|
||||
d = self.ser.read(1)
|
||||
bys = bys + d
|
||||
time.sleep(0.01)
|
||||
|
||||
if bys.__len__() < size:
|
||||
time.sleep(0.1)
|
||||
if self.ser.in_waiting > 0:
|
||||
d = self.ser.read(self.ser.in_waiting)
|
||||
bys = bys + d
|
||||
# print("readhex:" + str(binascii.hexlify(bys)) + " readlen=" + str(bys.__len__()))
|
||||
return bys
|
||||
except Exception as e:
|
||||
self.exception = e
|
||||
print('读串口异常', str(e))
|
||||
return None
|
||||
181
utils/caSerail.py
Normal file
181
utils/caSerail.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
import re
|
||||
import time
|
||||
from utils.baseSerail import BaseSerial
|
||||
# from baseSerail import BaseSerial
|
||||
import colour
|
||||
|
||||
class CASerail(BaseSerial):
|
||||
def __init__(self):
|
||||
BaseSerial.__init__(self)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def checkport(self):
|
||||
if not self.ser.is_open:
|
||||
self.reOpen()
|
||||
|
||||
return self.ser.is_open
|
||||
|
||||
def sendcmd(self, cmd:str):
|
||||
self.write(cmd.encode('utf-8'))
|
||||
return self.read()
|
||||
|
||||
'''开启通讯'''
|
||||
def startCommunication(self):
|
||||
self.sendcmd('COM,1\r')
|
||||
|
||||
'''结束通讯'''
|
||||
def endCommunication(self):
|
||||
self.sendcmd('COM,0\r')
|
||||
|
||||
'''设置用于测量的校准通道'''
|
||||
def setChannel(self, channel:str):
|
||||
self.sendcmd('MCH,%s\r'%(channel))
|
||||
|
||||
'''
|
||||
设置显示模式
|
||||
'''
|
||||
def setDisplayMode(self, mode:int):
|
||||
return self.sendcmd('MDS,%d\r'%(mode))
|
||||
|
||||
'''设置显示模式为XYZ'''
|
||||
def set_XYZ_Display(self):
|
||||
return self.sendcmd('MDS,%d\r'%(7))
|
||||
|
||||
'''设置显示模式为xyLv'''
|
||||
def set_xyLv_Display(self):
|
||||
return self.sendcmd('MDS,%d\r'%(0))
|
||||
|
||||
'''设置显示模式为xyLv'''
|
||||
def set_all_Display(self):
|
||||
return self.sendcmd('MDS,%d\r'%(0))
|
||||
|
||||
def set_Display(self, mode:int):
|
||||
'''
|
||||
0: x,y,LV
|
||||
1: Tcp,duv,LV
|
||||
5: u’,v’,LV
|
||||
7: X,Y,Z
|
||||
8: λd,Pe,LV
|
||||
'''
|
||||
return self.sendcmd('MDS,%d\r'%(mode))
|
||||
|
||||
|
||||
'''
|
||||
设置同步模式和同步频率
|
||||
'''
|
||||
def setSynchMode(self, mode: int, freq: int = None):
|
||||
if freq == None:
|
||||
return self.sendcmd('SCS,%d\r'%(mode))
|
||||
else:
|
||||
return self.sendcmd('SCS,%d,%d\r'%(mode,freq))
|
||||
|
||||
'''
|
||||
设置测量速度
|
||||
'''
|
||||
def setMeasureSpeed(self, speed: int):
|
||||
return self.sendcmd('FSC,%d\r'%(speed))
|
||||
|
||||
'''执行零点校准'''
|
||||
def setZeroCalibration(self):
|
||||
return self.sendcmd('ZRC\r')
|
||||
|
||||
'''读取当前显示模式下的三值'''
|
||||
def readDisplay(self):
|
||||
data = self.sendcmd('MES\r')
|
||||
x,y,z = self.__get_display_data(data.decode('utf-8'))
|
||||
return float(x), float(y), float(z)
|
||||
|
||||
'''获取当前显示模式值'''
|
||||
def __get_display_data(self, data):
|
||||
p = re.compile(r"(\D*)(\d+),P1 (.*);(.*);(.*)", re.DOTALL)
|
||||
mo = p.search(data)
|
||||
if mo is None:
|
||||
print("无匹配正则")
|
||||
pw = re.compile(r"(\D*)(\d+)", re.DOTALL)
|
||||
mo = pw.search(data)
|
||||
if mo is None:
|
||||
# print("短匹配失败")
|
||||
return None, None, None
|
||||
else:
|
||||
return None, None, None
|
||||
else:
|
||||
# print(mo.group(1), mo.group(2), mo.group(3), mo.group(4), mo.group(5))
|
||||
return mo.group(3), mo.group(4), mo.group(5)
|
||||
|
||||
'''读取所有数据'''
|
||||
def readAllDisplay(self):
|
||||
data = self.sendcmd('MES,2\r')
|
||||
x,y,lv,X,Y,Z = self.__get_all_display_data(data.decode('utf-8'))
|
||||
return float(x), float(y), float(lv), float(X), float(Y), float(Z)
|
||||
|
||||
'''获取所有显示模式值'''
|
||||
def __get_all_display_data(self, data):
|
||||
p = re.compile(r"(\D*)(\d+),P1,(.*),(.*),(.*),(.*),(.*),(.*),(.*),(.*),(.*)", re.DOTALL)
|
||||
mo = p.search(data)
|
||||
if mo is None:
|
||||
return None, None, None, None, None, None
|
||||
else:
|
||||
return mo.group(4), mo.group(5), mo.group(6), mo.group(9), mo.group(10), mo.group(11)
|
||||
|
||||
def xyY_to_XYZ( x, y, Y):
|
||||
X = x * Y / y
|
||||
Z = (1 - x - y) * Y / y
|
||||
return X, Y, Z
|
||||
|
||||
|
||||
def XYZ_to_lambda_pe(XYZ, illuminant='D65'):
|
||||
xy = colour.XYZ_to_xy(XYZ)
|
||||
white = colour.CCS_ILLUMINANTS['CIE 1931 2 Degree Standard Observer'][illuminant]
|
||||
λd, comp, xy_intersection = colour.dominant_wavelength(xy, white)
|
||||
Pe = colour.excitation_purity(xy, white)
|
||||
return xy, λd, comp, Pe
|
||||
|
||||
|
||||
def test_ca410():
|
||||
print('\r\rtest ca410\r\r')
|
||||
ca = CASerail()
|
||||
ca.open("COM3", 19200, 7, 'E', 2)
|
||||
ca.startCommunication()
|
||||
ca.setSynchMode(3)
|
||||
ca.setMeasureSpeed(2)
|
||||
ca.setZeroCalibration()
|
||||
ca.setChannel('01')
|
||||
|
||||
ca.set_Display(0)
|
||||
xyYXYZ = ca.readAllDisplay()
|
||||
print(f"xyYXYZ: {xyYXYZ}")
|
||||
XYZ = xyYXYZ[3], xyYXYZ[4], xyYXYZ[5]
|
||||
xy = colour.XYZ_to_xy(XYZ)
|
||||
print(f"xy: {xy}")
|
||||
|
||||
ca.set_Display(1)
|
||||
tcpduvlvXYZ = ca.readAllDisplay()
|
||||
print(f"tcpduvlvXYZ: {tcpduvlvXYZ}")
|
||||
XYZ = tcpduvlvXYZ[3], tcpduvlvXYZ[4], tcpduvlvXYZ[5]
|
||||
xy = colour.XYZ_to_xy(XYZ)
|
||||
TCP = colour.temperature.xy_to_CCT(xy, 'Hernandez 1999')
|
||||
print(f"TCP: {TCP}K")
|
||||
|
||||
ca.set_Display(5)
|
||||
uvlvXYZ = ca.readAllDisplay()
|
||||
print(f"uvlvXYZ: {uvlvXYZ}")
|
||||
XYZ = uvlvXYZ[3], uvlvXYZ[4], uvlvXYZ[5]
|
||||
xy = colour.XYZ_to_xy(XYZ)
|
||||
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(XYZ)
|
||||
print(f"u': {u_prime}, v': {v_prime}")
|
||||
|
||||
|
||||
ca.set_Display(8)
|
||||
lambdaPeXYZ = ca.readAllDisplay()
|
||||
print(f"lambdaPeXYZ: {lambdaPeXYZ}")
|
||||
XYZ = lambdaPeXYZ[3], lambdaPeXYZ[4], lambdaPeXYZ[5]
|
||||
xy, λd, isComplementary, Pe = XYZ_to_lambda_pe(XYZ)
|
||||
print(f"xy: {xy}, λd: {λd}, isComplementary: {isComplementary}, Pe: {Pe}")
|
||||
|
||||
|
||||
ca.endCommunication()
|
||||
|
||||
# test_ca410()
|
||||
288
utils/data_range_converter.py
Normal file
288
utils/data_range_converter.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
"""
|
||||
数据范围转换器
|
||||
将 Full Range (0-255) 转换为 Limited Range (16-235)
|
||||
|
||||
使用方法:
|
||||
from utils.data_range_converter import DataRangeConverter
|
||||
|
||||
converter = DataRangeConverter()
|
||||
converted_params = converter.convert(pattern_params, "Limited")
|
||||
"""
|
||||
|
||||
|
||||
class DataRangeConverter:
|
||||
"""数据范围转换器"""
|
||||
|
||||
def __init__(self, verbose=True):
|
||||
"""
|
||||
初始化转换器
|
||||
|
||||
Args:
|
||||
verbose: 是否打印详细日志
|
||||
"""
|
||||
self.verbose = verbose
|
||||
|
||||
def convert_value(self, value):
|
||||
"""
|
||||
将单个 Full Range 值转换为 Limited Range
|
||||
|
||||
转换公式:
|
||||
limited = 16 + (value / 255) × (235 - 16)
|
||||
|
||||
Args:
|
||||
value: Full Range 值 (0-255)
|
||||
|
||||
Returns:
|
||||
int: Limited Range 值 (16-235)
|
||||
"""
|
||||
# 边界值直接映射
|
||||
if value == 0:
|
||||
return 16
|
||||
elif value == 255:
|
||||
return 235
|
||||
else:
|
||||
# 线性映射
|
||||
limited = 16 + round((value / 255.0) * (235 - 16))
|
||||
# 限制范围
|
||||
return max(16, min(235, limited))
|
||||
|
||||
def convert_rgb(self, r, g, b):
|
||||
"""
|
||||
转换单个 RGB 值
|
||||
|
||||
Args:
|
||||
r, g, b: Full Range RGB 值 (0-255)
|
||||
|
||||
Returns:
|
||||
tuple: Limited Range RGB 值 (16-235)
|
||||
"""
|
||||
return (self.convert_value(r), self.convert_value(g), self.convert_value(b))
|
||||
|
||||
def convert(self, pattern_params, data_range="Full"):
|
||||
"""
|
||||
转换图案参数列表
|
||||
|
||||
Args:
|
||||
pattern_params: 图案参数列表 [[r,g,b], [r,g,b], ...]
|
||||
data_range: "Full" 或 "Limited"
|
||||
|
||||
Returns:
|
||||
list: 转换后的图案参数列表
|
||||
"""
|
||||
# Full Range 不需要转换
|
||||
if data_range == "Full":
|
||||
if self.verbose:
|
||||
print("✓ 使用 Full Range (0-255),无需转换")
|
||||
return pattern_params
|
||||
|
||||
# Limited Range 需要转换
|
||||
if data_range == "Limited":
|
||||
if self.verbose:
|
||||
self._print_header()
|
||||
|
||||
converted = []
|
||||
for i, rgb in enumerate(pattern_params):
|
||||
# 获取原始值
|
||||
r_orig, g_orig, b_orig = rgb[0], rgb[1], rgb[2]
|
||||
|
||||
# 转换
|
||||
r_new, g_new, b_new = self.convert_rgb(r_orig, g_orig, b_orig)
|
||||
|
||||
# 保存
|
||||
converted.append([r_new, g_new, b_new])
|
||||
|
||||
# 打印日志(关键值)
|
||||
if self.verbose:
|
||||
self._print_conversion(
|
||||
i, r_orig, g_orig, b_orig, r_new, g_new, b_new
|
||||
)
|
||||
|
||||
if self.verbose:
|
||||
self._print_footer(len(pattern_params))
|
||||
|
||||
return converted
|
||||
|
||||
# 未知范围,返回原始值
|
||||
else:
|
||||
if self.verbose:
|
||||
print(f"⚠ 未知的数据范围: {data_range},使用原始值")
|
||||
return pattern_params
|
||||
|
||||
def _print_header(self):
|
||||
"""打印转换头部信息"""
|
||||
print("=" * 80)
|
||||
print("【数据范围转换】Limited Range (16-235)")
|
||||
print(" 转换公式: 16 + (value / 255) × (235 - 16)")
|
||||
print("=" * 80)
|
||||
|
||||
def _print_conversion(self, index, r_orig, g_orig, b_orig, r_new, g_new, b_new):
|
||||
"""
|
||||
打印转换日志
|
||||
|
||||
策略:只打印关键值,避免刷屏
|
||||
- 第一个和最后一个图案
|
||||
- 0, 128, 255 等关键值
|
||||
"""
|
||||
# 判断是否需要打印
|
||||
should_print = False
|
||||
|
||||
# 第一个和最后一个
|
||||
if index == 0:
|
||||
should_print = True
|
||||
label = "黑色"
|
||||
elif index == len([]) - 1: # 需要在外部判断
|
||||
should_print = True
|
||||
label = "白色"
|
||||
# 关键 RGB 值
|
||||
elif (
|
||||
r_orig in [0, 128, 255]
|
||||
or g_orig in [0, 128, 255]
|
||||
or b_orig in [0, 128, 255]
|
||||
):
|
||||
should_print = True
|
||||
if r_orig == 128:
|
||||
label = "50% 灰"
|
||||
else:
|
||||
label = ""
|
||||
|
||||
if should_print:
|
||||
diff = abs(r_new - r_orig)
|
||||
print(
|
||||
f" 图案 {index+1:2d} {label:8s}: "
|
||||
f"RGB({r_orig:3d},{g_orig:3d},{b_orig:3d}) → "
|
||||
f"RGB({r_new:3d},{g_new:3d},{b_new:3d}) "
|
||||
f"(差值: {diff:+3d})"
|
||||
)
|
||||
|
||||
def _print_footer(self, total_count):
|
||||
"""打印转换尾部信息"""
|
||||
print(f"✓ 转换完成,共 {total_count} 个图案")
|
||||
print("=" * 80)
|
||||
|
||||
def get_info(self):
|
||||
"""获取转换器信息"""
|
||||
return {
|
||||
"name": "Data Range Converter",
|
||||
"version": "1.0.0",
|
||||
"full_range": "0-255",
|
||||
"limited_range": "16-235",
|
||||
"formula": "16 + (value / 255) × 219",
|
||||
}
|
||||
|
||||
|
||||
# ========== 便捷函数 ==========
|
||||
|
||||
|
||||
def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
|
||||
"""
|
||||
便捷函数:转换图案参数
|
||||
|
||||
Args:
|
||||
pattern_params: 图案参数列表 [[r,g,b], [r,g,b], ...]
|
||||
data_range: "Full" 或 "Limited"
|
||||
verbose: 是否打印日志
|
||||
|
||||
Returns:
|
||||
list: 转换后的图案参数列表
|
||||
|
||||
示例:
|
||||
>>> from utils.data_range_converter import convert_pattern_params
|
||||
>>> params = [[0,0,0], [255,255,255]]
|
||||
>>> converted = convert_pattern_params(params, "Limited")
|
||||
[[16,16,16], [235,235,235]]
|
||||
"""
|
||||
converter = DataRangeConverter(verbose=verbose)
|
||||
return converter.convert(pattern_params, data_range)
|
||||
|
||||
|
||||
def convert_single_rgb(r, g, b, data_range="Full"):
|
||||
"""
|
||||
便捷函数:转换单个 RGB 值
|
||||
|
||||
Args:
|
||||
r, g, b: RGB 值 (0-255)
|
||||
data_range: "Full" 或 "Limited"
|
||||
|
||||
Returns:
|
||||
tuple: 转换后的 RGB 值
|
||||
|
||||
示例:
|
||||
>>> from utils.data_range_converter import convert_single_rgb
|
||||
>>> r, g, b = convert_single_rgb(0, 0, 0, "Limited")
|
||||
(16, 16, 16)
|
||||
"""
|
||||
if data_range == "Full":
|
||||
return (r, g, b)
|
||||
|
||||
converter = DataRangeConverter(verbose=False)
|
||||
return converter.convert_rgb(r, g, b)
|
||||
|
||||
|
||||
# ========== 测试代码 ==========
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""测试转换器"""
|
||||
|
||||
print("=" * 80)
|
||||
print("数据范围转换器 - 测试")
|
||||
print("=" * 80)
|
||||
|
||||
# 测试 1: 基本转换
|
||||
print("\n[测试 1] 基本转换...")
|
||||
converter = DataRangeConverter(verbose=False)
|
||||
|
||||
test_values = [0, 16, 64, 128, 192, 235, 255]
|
||||
print(" Full Range → Limited Range:")
|
||||
for v in test_values:
|
||||
limited = converter.convert_value(v)
|
||||
diff = limited - v
|
||||
print(f" {v:3d} → {limited:3d} (差值: {diff:+3d})")
|
||||
|
||||
# 测试 2: RGB 转换
|
||||
print("\n[测试 2] RGB 转换...")
|
||||
test_rgb = [
|
||||
(0, 0, 0),
|
||||
(128, 128, 128),
|
||||
(255, 255, 255),
|
||||
]
|
||||
|
||||
for r, g, b in test_rgb:
|
||||
r_new, g_new, b_new = converter.convert_rgb(r, g, b)
|
||||
print(f" RGB({r},{g},{b}) → RGB({r_new},{g_new},{b_new})")
|
||||
|
||||
# 测试 3: 完整转换流程
|
||||
print("\n[测试 3] 完整转换流程...")
|
||||
pattern_params = [
|
||||
[255, 255, 255], # 100% 白
|
||||
[230, 230, 230], # 90%
|
||||
[204, 204, 204], # 80%
|
||||
[128, 128, 128], # 50%
|
||||
[0, 0, 0], # 0% 黑
|
||||
]
|
||||
|
||||
converted = converter.convert(pattern_params, "Limited")
|
||||
|
||||
print("\n 对比:")
|
||||
for i, (orig, conv) in enumerate(zip(pattern_params, converted)):
|
||||
print(f" [{i+1}] {orig} → {conv}")
|
||||
|
||||
# 测试 4: 便捷函数
|
||||
print("\n[测试 4] 便捷函数...")
|
||||
result = convert_pattern_params(
|
||||
[[0, 0, 0], [255, 255, 255]], "Limited", verbose=False
|
||||
)
|
||||
print(f" 结果: {result}")
|
||||
|
||||
r, g, b = convert_single_rgb(128, 128, 128, "Limited")
|
||||
print(f" RGB(128,128,128) → RGB({r},{g},{b})")
|
||||
|
||||
# 测试 5: 获取信息
|
||||
print("\n[测试 5] 转换器信息...")
|
||||
info = converter.get_info()
|
||||
for key, value in info.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("测试完成")
|
||||
print("=" * 80)
|
||||
585
utils/local_dimming_test.py
Normal file
585
utils/local_dimming_test.py
Normal file
@@ -0,0 +1,585 @@
|
||||
"""
|
||||
Local Dimming 测试模块
|
||||
功能:
|
||||
- 生成不同百分比的白色窗口图片
|
||||
- 通过 UCD 发送图片到显示器
|
||||
- 自动采集 CA410 亮度数据
|
||||
- 记录并导出测试结果
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import atexit
|
||||
import shutil
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
import UniTAP
|
||||
|
||||
|
||||
class LocalDimmingController:
|
||||
"""Local Dimming 控制器 - 用于发送不同百分比窗口 Pattern"""
|
||||
|
||||
def __init__(self, ucd_controller):
|
||||
"""
|
||||
初始化 Local Dimming 控制器
|
||||
|
||||
Args:
|
||||
ucd_controller: UCD323 控制器实例
|
||||
"""
|
||||
self.ucd = ucd_controller
|
||||
|
||||
# 兼容打包后的路径
|
||||
if getattr(sys, "frozen", False):
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base_dir = os.getcwd()
|
||||
|
||||
self.temp_dir = os.path.join(base_dir, "temp_local_dimming")
|
||||
|
||||
# 创建临时目录
|
||||
if not os.path.exists(self.temp_dir):
|
||||
os.makedirs(self.temp_dir)
|
||||
print(f"[LD] 创建临时目录: {self.temp_dir}")
|
||||
|
||||
self.cached_images = {} # 缓存已生成的图片 {(分辨率, 百分比): 文件路径}
|
||||
|
||||
# 注册退出时自动清理
|
||||
atexit.register(self.cleanup)
|
||||
|
||||
print("[LD] Local Dimming 控制器已初始化")
|
||||
|
||||
def send_window_pattern_with_resolution(self, percentage, width, height):
|
||||
"""
|
||||
发送指定百分比和分辨率的白色窗口 Pattern
|
||||
|
||||
Args:
|
||||
percentage: 窗口面积百分比 (1-100)
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
# 检查设备连接状态
|
||||
if not self.ucd.status:
|
||||
print("[LD 错误] 设备未连接")
|
||||
return False
|
||||
|
||||
print(f"\n[LD] 开始发送 {percentage}% 窗口 Pattern")
|
||||
print(f"[LD] 使用分辨率: {width}x{height}")
|
||||
|
||||
# 获取 Pattern Generator 和 Audio Generator
|
||||
# 兼容 UCDController(仅 HDMI)和 UCD323Controller(多接口)
|
||||
if hasattr(self.ucd, 'current_interface'):
|
||||
# UCD323Controller(多接口支持)
|
||||
interface = self.ucd.current_interface
|
||||
if interface == "HDMI":
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
ag = self.ucd.role.hdtx.ag
|
||||
elif interface == "Type-C" or interface == "DP":
|
||||
pg = self.ucd.role.dptx.pg
|
||||
ag = self.ucd.role.dptx.ag
|
||||
else:
|
||||
print(f"[LD 错误] 不支持的接口类型: {interface}")
|
||||
return False
|
||||
else:
|
||||
# UCDController(仅 HDMI)
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
ag = self.ucd.role.hdtx.ag
|
||||
|
||||
# 先停止音频,避免蜂鸣声
|
||||
try:
|
||||
ag.stop_generate()
|
||||
print("[LD] 已停止音频生成")
|
||||
except Exception as e:
|
||||
print(f"[LD 警告] 停止音频失败: {e}")
|
||||
|
||||
# 检查缓存
|
||||
cache_key = (f"{width}x{height}", percentage)
|
||||
|
||||
if cache_key in self.cached_images:
|
||||
image_path = self.cached_images[cache_key]
|
||||
if os.path.exists(image_path):
|
||||
print(f"[LD] 使用缓存图片: {image_path}")
|
||||
else:
|
||||
print(f"[LD] 缓存图片不存在,重新生成...")
|
||||
image_path = self._generate_and_save_image(
|
||||
width, height, percentage, cache_key
|
||||
)
|
||||
else:
|
||||
print(f"[LD] 正在生成 {percentage}% 窗口图像...")
|
||||
image_path = self._generate_and_save_image(
|
||||
width, height, percentage, cache_key
|
||||
)
|
||||
|
||||
# 发送图像到设备
|
||||
print(f"[LD] 正在发送图像到设备...")
|
||||
|
||||
# 设置 ColorInfo
|
||||
color_mode = UniTAP.ColorInfo()
|
||||
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
||||
color_mode.bpc = 8
|
||||
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
|
||||
|
||||
# 获取当前 timing
|
||||
try:
|
||||
current_vm = pg.get_vm()
|
||||
timing = (
|
||||
current_vm.timing
|
||||
if current_vm and hasattr(current_vm, "timing")
|
||||
else None
|
||||
)
|
||||
except:
|
||||
timing = None
|
||||
|
||||
# 如果有 timing,设置 VideoMode
|
||||
if timing:
|
||||
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
|
||||
pg.set_vm(vm=video_mode)
|
||||
|
||||
# 设置图片 Pattern
|
||||
pg.set_pattern(pattern=image_path)
|
||||
|
||||
# 应用
|
||||
pg.apply()
|
||||
|
||||
print(f"[LD] {percentage}% 窗口 Pattern 已发送到设备")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LD 异常] 发送 {percentage}% 窗口失败: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def send_window_pattern(self, percentage):
|
||||
"""
|
||||
发送指定百分比的白色窗口 Pattern(从 GUI 获取分辨率)
|
||||
|
||||
Args:
|
||||
percentage: 窗口面积百分比 (1-100)
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
# 从设备当前 timing 获取分辨率
|
||||
width, height = self.get_current_resolution()
|
||||
return self.send_window_pattern_with_resolution(percentage, width, height)
|
||||
|
||||
def get_current_resolution(self):
|
||||
"""
|
||||
从设备当前 timing 获取显示器分辨率
|
||||
|
||||
Returns:
|
||||
tuple: (width, height)
|
||||
"""
|
||||
try:
|
||||
# 方式1:从 Pattern Generator 的当前 VideoMode 获取
|
||||
if hasattr(self.ucd, 'current_interface'):
|
||||
interface = self.ucd.current_interface
|
||||
if interface == "HDMI":
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
elif interface == "Type-C" or interface == "DP":
|
||||
pg = self.ucd.role.dptx.pg
|
||||
else:
|
||||
pg = None
|
||||
else:
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
|
||||
if pg:
|
||||
current_vm = pg.get_vm()
|
||||
if current_vm and hasattr(current_vm, "timing") and current_vm.timing:
|
||||
timing = current_vm.timing
|
||||
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
|
||||
width = timing.h_active
|
||||
height = timing.v_active
|
||||
print(f"[LD] 从当前 timing 获取分辨率: {width}x{height}")
|
||||
return width, height
|
||||
|
||||
# 方式2:从 current_timing 属性获取
|
||||
if hasattr(self.ucd, "current_timing") and self.ucd.current_timing:
|
||||
timing = self.ucd.current_timing
|
||||
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
|
||||
width = timing.h_active
|
||||
height = timing.v_active
|
||||
print(f"[LD] 从 current_timing 获取分辨率: {width}x{height}")
|
||||
return width, height
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LD 警告] 获取分辨率失败: {e}")
|
||||
|
||||
print("[LD 警告] 使用默认分辨率 3840x2160")
|
||||
return 3840, 2160
|
||||
|
||||
def _generate_and_save_image(self, width, height, percentage, cache_key):
|
||||
"""
|
||||
生成并保存窗口图像
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
percentage: 窗口面积百分比
|
||||
cache_key: 缓存键
|
||||
|
||||
Returns:
|
||||
str: 图像文件路径
|
||||
"""
|
||||
# 生成图像
|
||||
image_array = self._create_window_image(width, height, percentage)
|
||||
|
||||
# 保存到项目目录
|
||||
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
|
||||
image_path = os.path.join(self.temp_dir, filename)
|
||||
|
||||
image = Image.fromarray(image_array, mode="RGB")
|
||||
image.save(image_path, format="PNG")
|
||||
|
||||
# 缓存
|
||||
self.cached_images[cache_key] = image_path
|
||||
|
||||
print(f"[LD] 图像已保存: {image_path}")
|
||||
return image_path
|
||||
|
||||
def _create_window_image(self, width, height, percentage):
|
||||
"""
|
||||
创建窗口图像
|
||||
黑色背景 + 居中白色矩形窗口(保持屏幕比例)
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
percentage: 窗口面积百分比 (1-100)
|
||||
|
||||
Returns:
|
||||
numpy.ndarray: RGB 图像数组 (height, width, 3)
|
||||
"""
|
||||
# 创建黑色背景
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
# 计算窗口尺寸(保持屏幕比例)
|
||||
scale_factor = (percentage / 100.0) ** 0.5
|
||||
window_width = int(width * scale_factor)
|
||||
window_height = int(height * scale_factor)
|
||||
|
||||
# 100% 时强制全屏
|
||||
if percentage == 100:
|
||||
window_width = width
|
||||
window_height = height
|
||||
|
||||
# 计算居中位置
|
||||
x1 = (width - window_width) // 2
|
||||
y1 = (height - window_height) // 2
|
||||
x2 = x1 + window_width
|
||||
y2 = y1 + window_height
|
||||
|
||||
# 绘制白色窗口
|
||||
image[y1:y2, x1:x2] = [255, 255, 255]
|
||||
|
||||
print(
|
||||
f"[LD] 图像生成完成: {width}x{height}, 窗口 {window_width}x{window_height}"
|
||||
)
|
||||
return image
|
||||
|
||||
def cleanup(self):
|
||||
"""清理临时文件夹"""
|
||||
if os.path.exists(self.temp_dir):
|
||||
try:
|
||||
shutil.rmtree(self.temp_dir)
|
||||
print(f"[LD] 临时文件夹已删除: {self.temp_dir}")
|
||||
except Exception as e:
|
||||
print(f"[LD 警告] 删除临时文件夹失败: {e}")
|
||||
|
||||
def __del__(self):
|
||||
"""析构函数:清理临时文件(备用机制)"""
|
||||
try:
|
||||
self.cleanup()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class LocalDimmingTest:
|
||||
def __init__(self, ucd_controller, ca_serial, log_callback=None):
|
||||
"""
|
||||
初始化 Local Dimming 测试
|
||||
|
||||
Args:
|
||||
ucd_controller: UCD323 控制器实例
|
||||
ca_serial: CA410 串口实例
|
||||
log_callback: 日志回调函数
|
||||
"""
|
||||
self.ucd = ucd_controller
|
||||
self.ca = ca_serial
|
||||
self.log = log_callback if log_callback else print
|
||||
|
||||
# 临时图片目录
|
||||
self.temp_dir = self._init_temp_dir()
|
||||
|
||||
# 测试结果
|
||||
self.test_results = []
|
||||
|
||||
# 测试配置
|
||||
self.window_percentages = [1, 2, 5, 10, 18, 25, 50, 75, 100]
|
||||
self.wait_time = 2.0 # 每次切换后等待时间(秒)
|
||||
|
||||
# 停止标志
|
||||
self.stop_flag = False
|
||||
|
||||
self.log("✓ Local Dimming 测试模块已初始化")
|
||||
|
||||
def _init_temp_dir(self):
|
||||
"""初始化临时目录"""
|
||||
if getattr(sys, "frozen", False):
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base_dir = os.getcwd()
|
||||
|
||||
temp_dir = os.path.join(base_dir, "temp_local_dimming")
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
return temp_dir
|
||||
|
||||
def generate_window_image(self, width, height, percentage):
|
||||
"""
|
||||
生成窗口图片(黑色背景 + 居中白色矩形窗口)
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
percentage: 窗口面积百分比 (1-100)
|
||||
|
||||
Returns:
|
||||
str: 图片文件路径
|
||||
"""
|
||||
# 计算窗口尺寸(保持屏幕比例)
|
||||
scale_factor = (percentage / 100.0) ** 0.5
|
||||
window_width = int(width * scale_factor)
|
||||
window_height = int(height * scale_factor)
|
||||
|
||||
# 100% 时强制全屏
|
||||
if percentage == 100:
|
||||
window_width = width
|
||||
window_height = height
|
||||
|
||||
# 创建黑色背景
|
||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||
|
||||
# 计算居中位置
|
||||
x1 = (width - window_width) // 2
|
||||
y1 = (height - window_height) // 2
|
||||
x2 = x1 + window_width
|
||||
y2 = y1 + window_height
|
||||
|
||||
# 绘制白色窗口
|
||||
image[y1:y2, x1:x2] = [255, 255, 255]
|
||||
|
||||
# 保存图片
|
||||
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
|
||||
image_path = os.path.join(self.temp_dir, filename)
|
||||
|
||||
pil_image = Image.fromarray(image, mode="RGB")
|
||||
pil_image.save(image_path, format="PNG")
|
||||
|
||||
self.log(f" ✓ 图片已生成: {window_width}×{window_height} px")
|
||||
return image_path
|
||||
|
||||
def send_image_to_ucd(self, image_path):
|
||||
"""
|
||||
通过 UCD 发送图片到显示器
|
||||
|
||||
Args:
|
||||
image_path: 图片文件路径
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
# 获取 Pattern Generator 和 Audio Generator
|
||||
# 兼容 UCDController(仅 HDMI)和 UCD323Controller(多接口)
|
||||
if hasattr(self.ucd, 'current_interface'):
|
||||
interface = self.ucd.current_interface
|
||||
if interface == "HDMI":
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
ag = self.ucd.role.hdtx.ag
|
||||
elif interface == "Type-C" or interface == "DP":
|
||||
pg = self.ucd.role.dptx.pg
|
||||
ag = self.ucd.role.dptx.ag
|
||||
else:
|
||||
self.log(f" ❌ 不支持的接口类型: {interface}")
|
||||
return False
|
||||
else:
|
||||
# UCDController(仅 HDMI)
|
||||
pg = self.ucd.role.hdtx.pg
|
||||
ag = self.ucd.role.hdtx.ag
|
||||
|
||||
# 停止音频
|
||||
try:
|
||||
ag.stop_generate()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 设置 ColorInfo
|
||||
color_mode = UniTAP.ColorInfo()
|
||||
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
|
||||
color_mode.bpc = 8
|
||||
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
|
||||
|
||||
# 获取当前 timing
|
||||
try:
|
||||
current_vm = pg.get_vm()
|
||||
timing = (
|
||||
current_vm.timing
|
||||
if current_vm and hasattr(current_vm, "timing")
|
||||
else None
|
||||
)
|
||||
except:
|
||||
timing = None
|
||||
|
||||
# 设置 VideoMode
|
||||
if timing:
|
||||
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
|
||||
pg.set_vm(vm=video_mode)
|
||||
|
||||
# 设置图片 Pattern
|
||||
pg.set_pattern(pattern=image_path)
|
||||
|
||||
# 应用
|
||||
pg.apply()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ❌ 发送图片失败: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def measure_luminance(self):
|
||||
"""
|
||||
使用 CA410 采集亮度
|
||||
|
||||
Returns:
|
||||
tuple: (x, y, lv, X, Y, Z) 或 None
|
||||
"""
|
||||
try:
|
||||
if not self.ca:
|
||||
self.log(" ❌ CA410 未连接")
|
||||
return None
|
||||
|
||||
# 采集数据
|
||||
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
|
||||
|
||||
if x is not None and y is not None and lv is not None:
|
||||
self.log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
|
||||
return (x, y, lv, X, Y, Z)
|
||||
else:
|
||||
self.log(" ❌ 采集数据失败")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f" ❌ 采集亮度异常: {str(e)}")
|
||||
return None
|
||||
|
||||
def run_test(self, resolution="3840x2160"):
|
||||
"""
|
||||
执行完整的 Local Dimming 测试
|
||||
|
||||
Args:
|
||||
resolution: 分辨率字符串,如 "3840x2160"
|
||||
|
||||
Returns:
|
||||
list: 测试结果 [(百分比, x, y, lv, X, Y, Z), ...]
|
||||
"""
|
||||
self.log("=" * 60)
|
||||
self.log("开始 Local Dimming 测试")
|
||||
self.log("=" * 60)
|
||||
|
||||
# 重置停止标志
|
||||
self.stop_flag = False
|
||||
|
||||
# 解析分辨率
|
||||
try:
|
||||
width, height = map(int, resolution.split("x"))
|
||||
except:
|
||||
width, height = 3840, 2160
|
||||
self.log(f" ⚠️ 分辨率解析失败,使用默认值: {width}x{height}")
|
||||
|
||||
self.log(f" 分辨率: {width}x{height}")
|
||||
self.log(f" 测试窗口: {self.window_percentages}")
|
||||
self.log(f" 等待时间: {self.wait_time} 秒")
|
||||
self.log("")
|
||||
|
||||
self.test_results = []
|
||||
|
||||
for i, percentage in enumerate(self.window_percentages, start=1):
|
||||
# 检查停止标志
|
||||
if self.stop_flag:
|
||||
self.log("⚠️ 测试已停止")
|
||||
break
|
||||
|
||||
self.log(f"[{i}/{len(self.window_percentages)}] 测试 {percentage}% 窗口...")
|
||||
|
||||
# 1. 生成图片
|
||||
image_path = self.generate_window_image(width, height, percentage)
|
||||
|
||||
# 2. 发送到 UCD
|
||||
if not self.send_image_to_ucd(image_path):
|
||||
self.log(f" ❌ {percentage}% 窗口发送失败,跳过")
|
||||
continue
|
||||
|
||||
# 3. 等待稳定
|
||||
self.log(f" ⏳ 等待 {self.wait_time} 秒...")
|
||||
time.sleep(self.wait_time)
|
||||
|
||||
# 4. 采集亮度
|
||||
result = self.measure_luminance()
|
||||
|
||||
if result:
|
||||
x, y, lv, X, Y, Z = result
|
||||
self.test_results.append((percentage, x, y, lv, X, Y, Z))
|
||||
self.log(f" ✅ {percentage}% 窗口测试完成")
|
||||
else:
|
||||
self.log(f" ❌ {percentage}% 窗口采集失败")
|
||||
|
||||
self.log("")
|
||||
|
||||
self.log("=" * 60)
|
||||
self.log("✅ Local Dimming 测试完成")
|
||||
self.log(
|
||||
f" 成功测试: {len(self.test_results)}/{len(self.window_percentages)} 个窗口"
|
||||
)
|
||||
self.log("=" * 60)
|
||||
|
||||
return self.test_results
|
||||
|
||||
def stop(self):
|
||||
"""停止测试"""
|
||||
self.stop_flag = True
|
||||
self.log("⚠️ 正在停止测试...")
|
||||
|
||||
def get_results_summary(self):
|
||||
"""获取测试结果摘要"""
|
||||
if not self.test_results:
|
||||
return None
|
||||
|
||||
luminances = [lv for _, _, _, lv, _, _, _ in self.test_results]
|
||||
|
||||
return {
|
||||
"data_points": self.test_results,
|
||||
"max_luminance": max(luminances),
|
||||
"min_luminance": min(luminances),
|
||||
"avg_luminance": sum(luminances) / len(luminances),
|
||||
}
|
||||
|
||||
def cleanup(self):
|
||||
"""清理临时文件"""
|
||||
try:
|
||||
import shutil
|
||||
|
||||
if os.path.exists(self.temp_dir):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
self.log(f"✓ 临时文件已清理: {self.temp_dir}")
|
||||
except Exception as e:
|
||||
self.log(f"⚠️ 清理临时文件失败: {e}")
|
||||
601
utils/pq/pq_config.py
Normal file
601
utils/pq/pq_config.py
Normal file
@@ -0,0 +1,601 @@
|
||||
# PQ自动化测试配置模块
|
||||
import json
|
||||
import copy
|
||||
|
||||
|
||||
class PQConfig:
|
||||
def __init__(self, current_test_type="screen_module", device_config={}, pattern={}):
|
||||
self.default_test_types = {
|
||||
"screen_module": {
|
||||
"name": "屏模组性能测试",
|
||||
"test_items": ["gamut", "gamma", "cct", "contrast"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
},
|
||||
"sdr_movie": {
|
||||
"name": "SDR Movie测试",
|
||||
"test_items": ["gamut", "gamma", "cct", "contrast", "accuracy"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
},
|
||||
"hdr_movie": {
|
||||
"name": "HDR Movie测试",
|
||||
"test_items": ["gamut", "eotf", "cct", "contrast", "accuracy"],
|
||||
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||
"color_format": "RGB",
|
||||
"bpc": 8,
|
||||
"colorimetry": "sRGB",
|
||||
},
|
||||
}
|
||||
|
||||
# 设备连接配置
|
||||
self.device_config = {
|
||||
"ca_com": "COM1",
|
||||
"ucd_list": "0: UCD-323 [2128C209]",
|
||||
"ca_channel": "0",
|
||||
}
|
||||
|
||||
# ========== RGB Pattern 配置 ==========
|
||||
self.default_pattern_rgb = {
|
||||
"pattern_mode": "SolidColor",
|
||||
"measurement_bit_depth": 8,
|
||||
"measurement_max_value": 2,
|
||||
"pattern_params": [
|
||||
[255, 0, 0], # 红色
|
||||
[0, 255, 0], # 绿色
|
||||
[0, 0, 255], # 蓝色
|
||||
],
|
||||
}
|
||||
|
||||
# ========== 灰阶 Pattern 配置 ==========
|
||||
self.default_pattern_gray = {
|
||||
"pattern_mode": "SolidColor",
|
||||
"measurement_bit_depth": 8,
|
||||
"measurement_max_value": 10,
|
||||
"pattern_params": [
|
||||
[255, 255, 255], # 100% 白色
|
||||
[230, 230, 230], # 90%
|
||||
[205, 205, 205], # 80%
|
||||
[179, 179, 179], # 70%
|
||||
[154, 154, 154], # 60%
|
||||
[128, 128, 128], # 50%
|
||||
[102, 102, 102], # 40%
|
||||
[78, 78, 78], # 30%
|
||||
[52, 52, 52], # 20%
|
||||
[26, 26, 26], # 10%
|
||||
[0, 0, 0], # 0% 黑色
|
||||
],
|
||||
}
|
||||
|
||||
# ========== 色准 Pattern 配置(29色 - SDR 和 HDR 通用)==========
|
||||
self.default_pattern_accuracy = {
|
||||
"pattern_mode": "SolidColor",
|
||||
"measurement_bit_depth": 8,
|
||||
"measurement_max_value": 28, # 29个颜色,最大索引是28
|
||||
"pattern_params": [
|
||||
# ========== 灰阶 (5个) ==========
|
||||
[255, 255, 255], # 0: White
|
||||
[230, 230, 230], # 1: Gray 80
|
||||
[209, 209, 209], # 2: Gray 65
|
||||
[186, 186, 186], # 3: Gray 50
|
||||
[158, 158, 158], # 4: Gray 35
|
||||
# ========== ColorChecker 24色 (18个) ==========
|
||||
[115, 82, 66], # 5: Dark Skin
|
||||
[194, 150, 130], # 6: Light Skin
|
||||
[94, 122, 156], # 7: Blue Sky
|
||||
[89, 107, 66], # 8: Foliage
|
||||
[130, 128, 176], # 9: Blue Flower
|
||||
[99, 189, 168], # 10: Bluish Green
|
||||
[217, 120, 41], # 11: Orange
|
||||
[74, 92, 163], # 12: Purplish Blue
|
||||
[194, 84, 97], # 13: Moderate Red
|
||||
[92, 61, 107], # 14: Purple
|
||||
[158, 186, 64], # 15: Yellow Green
|
||||
[230, 161, 46], # 16: Orange Yellow
|
||||
[51, 61, 150], # 17: Blue (Legacy)
|
||||
[71, 148, 71], # 18: Green (Legacy)
|
||||
[176, 48, 59], # 19: Red (Legacy)
|
||||
[237, 199, 33], # 20: Yellow (Legacy)
|
||||
[186, 84, 145], # 21: Magenta (Legacy)
|
||||
[0, 133, 163], # 22: Cyan (Legacy)
|
||||
# ========== 100% 饱和色 (6个) ==========
|
||||
[255, 0, 0], # 23: 100% Red
|
||||
[0, 255, 0], # 24: 100% Green
|
||||
[0, 0, 255], # 25: 100% Blue
|
||||
[0, 255, 255], # 26: 100% Cyan
|
||||
[255, 0, 255], # 27: 100% Magenta
|
||||
[255, 255, 0], # 28: 100% Yellow
|
||||
],
|
||||
}
|
||||
|
||||
self.default_pattern_temp = {
|
||||
"pattern_mode": "SolidColor",
|
||||
"measurement_bit_depth": 8,
|
||||
"measurement_max_value": 146,
|
||||
"pattern_params": [
|
||||
[255, 255, 255],
|
||||
[242, 242, 242],
|
||||
[230, 230, 230],
|
||||
[217, 217, 217],
|
||||
[204, 204, 204],
|
||||
[191, 191, 191],
|
||||
[179, 179, 179],
|
||||
[166, 166, 166],
|
||||
[153, 153, 153],
|
||||
[140, 140, 140],
|
||||
[128, 128, 128],
|
||||
[115, 115, 115],
|
||||
[102, 102, 102],
|
||||
[89, 89, 89],
|
||||
[77, 77, 77],
|
||||
[64, 64, 64],
|
||||
[51, 51, 51],
|
||||
[38, 38, 38],
|
||||
[26, 26, 26],
|
||||
[13, 13, 13],
|
||||
[0, 0, 0],
|
||||
|
||||
[255, 0, 0],
|
||||
[242, 0, 0],
|
||||
[230, 0, 0],
|
||||
[217, 0, 0],
|
||||
[204, 0, 0],
|
||||
[191, 0, 0],
|
||||
[179, 0, 0],
|
||||
[166, 0, 0],
|
||||
[153, 0, 0],
|
||||
[140, 0, 0],
|
||||
[128, 0, 0],
|
||||
[115, 0, 0],
|
||||
[102, 0, 0],
|
||||
[89, 0, 0],
|
||||
[77, 0, 0],
|
||||
[64, 0, 0],
|
||||
[51, 0, 0],
|
||||
[38, 0, 0],
|
||||
[26, 0, 0],
|
||||
[13, 0, 0],
|
||||
[0, 0, 0],
|
||||
|
||||
[0, 255, 0],
|
||||
[0, 242, 0],
|
||||
[0, 230, 0],
|
||||
[0, 217, 0],
|
||||
[0, 204, 0],
|
||||
[0, 191, 0],
|
||||
[0, 179, 0],
|
||||
[0, 166, 0],
|
||||
[0, 153, 0],
|
||||
[0, 140, 0],
|
||||
[0, 128, 0],
|
||||
[0, 115, 0],
|
||||
[0, 102, 0],
|
||||
[0, 89, 0],
|
||||
[0, 77, 0],
|
||||
[0, 64, 0],
|
||||
[0, 51, 0],
|
||||
[0, 38, 0],
|
||||
[0, 26, 0],
|
||||
[0, 13, 0],
|
||||
[0, 0, 0],
|
||||
|
||||
[0, 0, 255],
|
||||
[0, 0, 242],
|
||||
[0, 0, 230],
|
||||
[0, 0, 217],
|
||||
[0, 0, 204],
|
||||
[0, 0, 191],
|
||||
[0, 0, 179],
|
||||
[0, 0, 166],
|
||||
[0, 0, 153],
|
||||
[0, 0, 140],
|
||||
[0, 0, 128],
|
||||
[0, 0, 115],
|
||||
[0, 0, 102],
|
||||
[0, 0, 89],
|
||||
[0, 0, 77],
|
||||
[0, 0, 64],
|
||||
[0, 0, 51],
|
||||
[0, 0, 38],
|
||||
[0, 0, 26],
|
||||
[0, 0, 13],
|
||||
[0, 0, 0],
|
||||
|
||||
[255, 255, 0],
|
||||
[242, 242, 0],
|
||||
[230, 230, 0],
|
||||
[217, 217, 0],
|
||||
[204, 204, 0],
|
||||
[191, 191, 0],
|
||||
[179, 179, 0],
|
||||
[166, 166, 0],
|
||||
[153, 153, 0],
|
||||
[140, 140, 0],
|
||||
[128, 128, 0],
|
||||
[115, 115, 0],
|
||||
[102, 102, 0],
|
||||
[89, 89, 0],
|
||||
[77, 77, 0],
|
||||
[64, 64, 0],
|
||||
[51, 51, 0],
|
||||
[38, 38, 0],
|
||||
[26, 26, 0],
|
||||
[13, 13, 0],
|
||||
[0, 0, 0],
|
||||
|
||||
[0, 255, 255],
|
||||
[0, 242, 242],
|
||||
[0, 230, 230],
|
||||
[0, 217, 217],
|
||||
[0, 204, 204],
|
||||
[0, 191, 191],
|
||||
[0, 179, 179],
|
||||
[0, 166, 166],
|
||||
[0, 153, 153],
|
||||
[0, 140, 140],
|
||||
[0, 128, 128],
|
||||
[0, 115, 115],
|
||||
[0, 102, 102],
|
||||
[0, 89, 89],
|
||||
[0, 77, 77],
|
||||
[0, 64, 64],
|
||||
[0, 51, 51],
|
||||
[0, 38, 38],
|
||||
[0, 26, 26],
|
||||
[0, 13, 13],
|
||||
[0, 0, 0],
|
||||
|
||||
[255, 0, 255],
|
||||
[242, 0, 242],
|
||||
[230, 0, 230],
|
||||
[217, 0, 217],
|
||||
[204, 0, 204],
|
||||
[191, 0, 191],
|
||||
[179, 0, 179],
|
||||
[166, 0, 166],
|
||||
[153, 0, 153],
|
||||
[140, 0, 140],
|
||||
[128, 0, 128],
|
||||
[115, 0, 115],
|
||||
[102, 0, 102],
|
||||
[89, 0, 89],
|
||||
[77, 0, 77],
|
||||
[64, 0, 64],
|
||||
[51, 0, 51],
|
||||
[38, 0, 38],
|
||||
[26, 0, 26],
|
||||
[13, 0, 13],
|
||||
[0, 0, 0],
|
||||
]
|
||||
}
|
||||
|
||||
# 自定义图案
|
||||
self.custom_pattern = {
|
||||
"pattern_mode": "SolidColor",
|
||||
"measurement_bit_depth": 8,
|
||||
"measurement_max_value": 0,
|
||||
"pattern_params": [],
|
||||
}
|
||||
|
||||
self.current_test_types = self.default_test_types
|
||||
self.current_test_type = current_test_type
|
||||
self.current_pattern = self.default_pattern_rgb
|
||||
|
||||
# ========== 获取临时配置(用于 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 模式
|
||||
if mode == "rgb":
|
||||
temp_config.current_pattern = copy.deepcopy(self.default_pattern_rgb)
|
||||
elif mode == "gray":
|
||||
temp_config.current_pattern = copy.deepcopy(self.default_pattern_gray)
|
||||
elif mode == "accuracy":
|
||||
temp_config.current_pattern = copy.deepcopy(self.default_pattern_accuracy)
|
||||
|
||||
# 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,
|
||||
"default_pattern_rgb": self.default_pattern_rgb,
|
||||
"default_pattern_gray": self.default_pattern_gray,
|
||||
"default_pattern_accuracy": self.default_pattern_accuracy,
|
||||
"custom_pattern": self.custom_pattern,
|
||||
}
|
||||
|
||||
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.default_pattern_rgb = config_dict.get(
|
||||
"default_pattern_rgb", self.default_pattern_rgb
|
||||
)
|
||||
self.default_pattern_gray = config_dict.get(
|
||||
"default_pattern_gray", self.default_pattern_gray
|
||||
)
|
||||
|
||||
# ========== ✅ 强制使用新的 29色配置 ==========
|
||||
loaded_accuracy = config_dict.get("default_pattern_accuracy", None)
|
||||
|
||||
# 检查加载的配置是否是旧的 10色
|
||||
if loaded_accuracy and len(loaded_accuracy.get("pattern_params", [])) != 29:
|
||||
print(
|
||||
f"⚠️ 检测到旧的配置({len(loaded_accuracy.get('pattern_params', []))}色),强制使用新的 29色配置"
|
||||
)
|
||||
# 使用 __init__ 中定义的新配置
|
||||
self.default_pattern_accuracy = self.default_pattern_accuracy
|
||||
else:
|
||||
self.default_pattern_accuracy = config_dict.get(
|
||||
"default_pattern_accuracy", self.default_pattern_accuracy
|
||||
)
|
||||
# ==========================================
|
||||
|
||||
self.custom_pattern = config_dict.get("custom_pattern", self.custom_pattern)
|
||||
|
||||
def save_to_file(self, filename):
|
||||
"""将配置保存到文件"""
|
||||
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):
|
||||
"""设置当前模式的测试图案"""
|
||||
if mode == "rgb":
|
||||
self.current_pattern = self.default_pattern_rgb
|
||||
elif mode == "gray":
|
||||
self.current_pattern = self.default_pattern_gray
|
||||
elif mode == "accuracy": # ✅ 色准模式(SDR 和 HDR 通用 29色)
|
||||
self.current_pattern = self.default_pattern_accuracy
|
||||
elif mode == "custom":
|
||||
# self.current_pattern = self.custom_pattern
|
||||
self.current_pattern = self.default_pattern_temp
|
||||
else:
|
||||
return False
|
||||
|
||||
# 确保 measurement_max_value 是整数
|
||||
if "measurement_max_value" in self.current_pattern:
|
||||
value = self.current_pattern["measurement_max_value"]
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
self.current_pattern["measurement_max_value"] = int(value)
|
||||
except ValueError:
|
||||
self.current_pattern["measurement_max_value"] = (
|
||||
len(self.current_pattern["pattern_params"]) - 1
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
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 = self.default_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(self.default_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):
|
||||
"""获取测试项目的显示名称"""
|
||||
item_names = []
|
||||
for item in test_items:
|
||||
if item == "gamut":
|
||||
item_names.append("色域")
|
||||
elif item == "gamma":
|
||||
item_names.append("Gamma")
|
||||
elif item == "eotf":
|
||||
item_names.append("EOTF")
|
||||
elif item == "cct":
|
||||
item_names.append("色度一致性")
|
||||
elif item == "contrast":
|
||||
item_names.append("对比度")
|
||||
elif item == "accuracy":
|
||||
item_names.append("色准")
|
||||
else:
|
||||
item_names.append(item)
|
||||
return item_names
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ========== ✅ 验证代码(测试完成后可删除)==========
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("验证 pq_config.py 配置")
|
||||
print("=" * 60)
|
||||
|
||||
config = PQConfig()
|
||||
|
||||
# 检查 default_pattern_accuracy
|
||||
pattern_count = len(config.default_pattern_accuracy["pattern_params"])
|
||||
max_value = config.default_pattern_accuracy["measurement_max_value"]
|
||||
|
||||
print(f"\ndefault_pattern_accuracy:")
|
||||
print(f" 图案数量: {pattern_count}")
|
||||
print(f" measurement_max_value: {max_value}")
|
||||
|
||||
if pattern_count == 29 and max_value == 28:
|
||||
print("\n✅ 配置正确(29色)")
|
||||
|
||||
# 显示前 5 个图案
|
||||
print("\n前5个图案:")
|
||||
for i in range(5):
|
||||
rgb = config.default_pattern_accuracy["pattern_params"][i]
|
||||
names = config.get_accuracy_color_names()
|
||||
print(f" [{i}] {names[i]:15s} RGB{rgb}")
|
||||
|
||||
# 显示后 5 个图案
|
||||
print("\n后5个图案:")
|
||||
for i in range(24, 29):
|
||||
rgb = config.default_pattern_accuracy["pattern_params"][i]
|
||||
names = config.get_accuracy_color_names()
|
||||
print(f" [{i}] {names[i]:15s} RGB{rgb}")
|
||||
|
||||
# 测试 set_current_pattern
|
||||
print("\n测试 set_current_pattern('accuracy'):")
|
||||
config.set_current_pattern("accuracy")
|
||||
current_count = len(config.current_pattern["pattern_params"])
|
||||
print(f" current_pattern 图案数量: {current_count}")
|
||||
|
||||
if current_count == 29:
|
||||
print(" ✅ set_current_pattern 工作正常")
|
||||
else:
|
||||
print(f" ❌ set_current_pattern 失败!只有 {current_count} 个图案")
|
||||
|
||||
else:
|
||||
print(f"\n❌ 配置错误!")
|
||||
print(f" 期望: 29 个图案, measurement_max_value=28")
|
||||
print(f" 实际: {pattern_count} 个图案, measurement_max_value={max_value}")
|
||||
|
||||
print("\n❌ 请检查 default_pattern_accuracy 定义!")
|
||||
print(" 应该包含:")
|
||||
print(" - 5个灰阶")
|
||||
print(" - 18个 ColorChecker 色块")
|
||||
print(" - 6个 100% 饱和色")
|
||||
print(" - 总计 29 个 RGB 数组")
|
||||
|
||||
print("=" * 60)
|
||||
469
utils/pq/pq_result.py
Normal file
469
utils/pq/pq_result.py
Normal file
@@ -0,0 +1,469 @@
|
||||
import json
|
||||
import os
|
||||
import datetime
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestItemResult:
|
||||
"""单个测试项的结果数据"""
|
||||
|
||||
item_name: str # 测试项名称 (如 "gamut", "gamma", "cct" 等)
|
||||
item_display_name: str # 测试项显示名称 (如 "色域", "Gamma", "色温一致性" 等)
|
||||
status: str # 测试状态: "completed", "failed", "skipped"
|
||||
start_time: Optional[datetime.datetime] = None
|
||||
end_time: Optional[datetime.datetime] = None
|
||||
intermediate_data: Dict[str, Any] = None # 中间过程数据
|
||||
final_result: Dict[str, Any] = None # 最终测试结果
|
||||
error_message: Optional[str] = None # 错误信息
|
||||
|
||||
def __post_init__(self):
|
||||
if self.intermediate_data is None:
|
||||
self.intermediate_data = {}
|
||||
if self.final_result is None:
|
||||
self.final_result = {}
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典格式"""
|
||||
data = asdict(self)
|
||||
if self.start_time:
|
||||
data["start_time"] = self.start_time.isoformat()
|
||||
if self.end_time:
|
||||
data["end_time"] = self.end_time.isoformat()
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""从字典创建对象"""
|
||||
if "start_time" in data and data["start_time"]:
|
||||
data["start_time"] = datetime.datetime.fromisoformat(data["start_time"])
|
||||
if "end_time" in data and data["end_time"]:
|
||||
data["end_time"] = datetime.datetime.fromisoformat(data["end_time"])
|
||||
return cls(**data)
|
||||
|
||||
|
||||
class PQResult:
|
||||
"""PQ测试结果管理类"""
|
||||
|
||||
def __init__(
|
||||
self, test_type: str = "", test_name: str = "", output_dir: str = "results"
|
||||
):
|
||||
"""
|
||||
初始化PQ测试结果管理器
|
||||
|
||||
Args:
|
||||
test_type: 测试类型 ("screen_module", "sdr_movie", "hdr_movie")
|
||||
test_name: 测试名称显示
|
||||
output_dir: 结果输出目录
|
||||
"""
|
||||
self.test_id = self._generate_test_id()
|
||||
self.test_type = test_type
|
||||
self.test_name = test_name
|
||||
self.output_dir = output_dir
|
||||
|
||||
# 测试基本信息
|
||||
self.start_time = datetime.datetime.now()
|
||||
self.end_time = None
|
||||
self.status = "running" # "running", "completed", "failed", "stopped"
|
||||
|
||||
# 测试配置信息
|
||||
self.test_config = {}
|
||||
|
||||
# 测试项结果
|
||||
self.test_items: Dict[str, TestItemResult] = {}
|
||||
|
||||
# 全局测试数据
|
||||
self.global_data = {
|
||||
"device_info": {},
|
||||
"environment_info": {},
|
||||
"measurement_settings": {},
|
||||
}
|
||||
|
||||
# 确保输出目录存在
|
||||
self._ensure_output_dir()
|
||||
|
||||
# =============================================================================
|
||||
# 存放当次测试的中间数据,方便调用
|
||||
# =============================================================================
|
||||
self.fix_pattern_rgb = None
|
||||
self.fix_pattern_gray = None
|
||||
|
||||
def _generate_test_id(self) -> str:
|
||||
"""生成唯一的测试ID"""
|
||||
return datetime.datetime.now().strftime("PQ_%Y%m%d_%H%M%S_%f")
|
||||
|
||||
def _ensure_output_dir(self):
|
||||
pass
|
||||
|
||||
def set_test_config(self, config: Dict[str, Any]):
|
||||
"""设置测试配置信息"""
|
||||
self.test_config = config.copy()
|
||||
|
||||
def set_global_data(
|
||||
self,
|
||||
device_info: Dict = None,
|
||||
environment_info: Dict = None,
|
||||
measurement_settings: Dict = None,
|
||||
):
|
||||
"""设置全局测试数据"""
|
||||
if device_info:
|
||||
self.global_data["device_info"].update(device_info)
|
||||
if environment_info:
|
||||
self.global_data["environment_info"].update(environment_info)
|
||||
if measurement_settings:
|
||||
self.global_data["measurement_settings"].update(measurement_settings)
|
||||
|
||||
def add_test_item(self, item_name: str, item_display_name: str) -> TestItemResult:
|
||||
"""添加测试项"""
|
||||
test_item = TestItemResult(
|
||||
item_name=item_name, item_display_name=item_display_name, status="pending"
|
||||
)
|
||||
self.test_items[item_name] = test_item
|
||||
return test_item
|
||||
|
||||
def start_test_item(self, item_name: str):
|
||||
"""开始测试项"""
|
||||
if item_name in self.test_items:
|
||||
self.test_items[item_name].status = "running"
|
||||
self.test_items[item_name].start_time = datetime.datetime.now()
|
||||
|
||||
def add_intermediate_data(self, item_name: str, data_key: str, data_value: Any):
|
||||
"""添加测试项的中间过程数据"""
|
||||
if item_name in self.test_items:
|
||||
self.test_items[item_name].intermediate_data[data_key] = data_value
|
||||
if data_key == "rgb":
|
||||
self.fix_pattern_rgb = data_value
|
||||
if data_key == "gray":
|
||||
self.fix_pattern_gray = data_value
|
||||
|
||||
def get_intermediate_data(self, item_name: str, data_key: str) -> Any:
|
||||
"""
|
||||
获取测试项的中间过程数据
|
||||
|
||||
Args:
|
||||
item_name: 测试项名称 (如 "gamut", "gamma", "cct", "shared")
|
||||
data_key: 数据键名 (如 "rgb", "gray", "measurement_points")
|
||||
|
||||
Returns:
|
||||
对应的数据,如果不存在则返回 None
|
||||
|
||||
Examples:
|
||||
>>> pq_result.get_intermediate_data("gamut", "rgb")
|
||||
[[0.64, 0.33, 100.5, ...], ...]
|
||||
|
||||
>>> pq_result.get_intermediate_data("shared", "gray")
|
||||
[[0.31, 0.33, 50.2, ...], ...]
|
||||
"""
|
||||
# 方式1: 从 test_items 中获取
|
||||
if item_name in self.test_items:
|
||||
intermediate_data = self.test_items[item_name].intermediate_data
|
||||
if data_key in intermediate_data:
|
||||
return intermediate_data[data_key]
|
||||
|
||||
# 方式2: 从快捷属性中获取(用于 "shared" 数据)
|
||||
if item_name == "shared":
|
||||
if data_key == "rgb" and self.fix_pattern_rgb is not None:
|
||||
return self.fix_pattern_rgb
|
||||
if data_key == "gray" and self.fix_pattern_gray is not None:
|
||||
return self.fix_pattern_gray
|
||||
|
||||
# 未找到数据
|
||||
return None
|
||||
|
||||
def has_intermediate_data(self, item_name: str, data_key: str) -> bool:
|
||||
"""
|
||||
检查是否存在指定的中间数据
|
||||
|
||||
Args:
|
||||
item_name: 测试项名称
|
||||
data_key: 数据键名
|
||||
|
||||
Returns:
|
||||
bool: 数据是否存在
|
||||
"""
|
||||
return self.get_intermediate_data(item_name, data_key) is not None
|
||||
|
||||
def get_all_intermediate_data(self, item_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取测试项的所有中间数据
|
||||
|
||||
Args:
|
||||
item_name: 测试项名称
|
||||
|
||||
Returns:
|
||||
包含所有中间数据的字典,如果测试项不存在则返回空字典
|
||||
"""
|
||||
if item_name in self.test_items:
|
||||
return self.test_items[item_name].intermediate_data.copy()
|
||||
|
||||
if item_name == "shared":
|
||||
return {
|
||||
"rgb": self.fix_pattern_rgb,
|
||||
"gray": self.fix_pattern_gray,
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
def clear_intermediate_data(self, item_name: str = None):
|
||||
"""
|
||||
清除中间数据
|
||||
|
||||
Args:
|
||||
item_name: 测试项名称,如果为 None 则清除所有中间数据
|
||||
"""
|
||||
if item_name is None:
|
||||
# 清除所有测试项的中间数据
|
||||
for item in self.test_items.values():
|
||||
item.intermediate_data.clear()
|
||||
# 清除快捷属性
|
||||
self.fix_pattern_rgb = None
|
||||
self.fix_pattern_gray = None
|
||||
elif item_name in self.test_items:
|
||||
# 清除指定测试项的中间数据
|
||||
self.test_items[item_name].intermediate_data.clear()
|
||||
|
||||
def set_test_item_result(
|
||||
self,
|
||||
item_name: str,
|
||||
result_data: Dict[str, Any],
|
||||
status: str = "completed",
|
||||
error_message: str = None,
|
||||
):
|
||||
"""设置测试项的最终结果"""
|
||||
if item_name in self.test_items:
|
||||
self.test_items[item_name].final_result = result_data
|
||||
self.test_items[item_name].status = status
|
||||
self.test_items[item_name].end_time = datetime.datetime.now()
|
||||
if error_message:
|
||||
self.test_items[item_name].error_message = error_message
|
||||
|
||||
def complete_test(self, status: str = "completed"):
|
||||
"""完成整个测试"""
|
||||
self.end_time = datetime.datetime.now()
|
||||
self.status = status
|
||||
|
||||
def get_test_summary(self) -> Dict[str, Any]:
|
||||
"""获取测试摘要信息"""
|
||||
completed_items = len(
|
||||
[item for item in self.test_items.values() if item.status == "completed"]
|
||||
)
|
||||
failed_items = len(
|
||||
[item for item in self.test_items.values() if item.status == "failed"]
|
||||
)
|
||||
total_items = len(self.test_items)
|
||||
|
||||
duration = None
|
||||
if self.start_time and self.end_time:
|
||||
duration = (self.end_time - self.start_time).total_seconds()
|
||||
|
||||
return {
|
||||
"test_id": self.test_id,
|
||||
"test_type": self.test_type,
|
||||
"test_name": self.test_name,
|
||||
"status": self.status,
|
||||
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||
"end_time": self.end_time.isoformat() if self.end_time else None,
|
||||
"duration_seconds": duration,
|
||||
"total_items": total_items,
|
||||
"completed_items": completed_items,
|
||||
"failed_items": failed_items,
|
||||
}
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为完整的字典格式"""
|
||||
return {
|
||||
"test_summary": self.get_test_summary(),
|
||||
"test_config": self.test_config,
|
||||
"global_data": self.global_data,
|
||||
"test_items": {
|
||||
name: item.to_dict() for name, item in self.test_items.items()
|
||||
},
|
||||
"export_timestamp": datetime.datetime.now().isoformat(),
|
||||
"format_version": "1.0",
|
||||
}
|
||||
|
||||
def save_to_file(self, file_path: str) -> bool:
|
||||
return False
|
||||
|
||||
def save_to_json(self, filename: str = None) -> str:
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def load_from_json(cls, file_path: str) -> "PQResult":
|
||||
"""从JSON文件加载测试结果"""
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 创建PQResult实例
|
||||
test_summary = data.get("test_summary", {})
|
||||
pq_result = cls(
|
||||
test_type=test_summary.get("test_type", ""),
|
||||
test_name=test_summary.get("test_name", ""),
|
||||
)
|
||||
|
||||
# 恢复基本信息
|
||||
pq_result.test_id = test_summary.get("test_id", pq_result.test_id)
|
||||
pq_result.status = test_summary.get("status", "unknown")
|
||||
|
||||
if test_summary.get("start_time"):
|
||||
pq_result.start_time = datetime.datetime.fromisoformat(
|
||||
test_summary["start_time"]
|
||||
)
|
||||
if test_summary.get("end_time"):
|
||||
pq_result.end_time = datetime.datetime.fromisoformat(
|
||||
test_summary["end_time"]
|
||||
)
|
||||
|
||||
# 恢复配置和全局数据
|
||||
pq_result.test_config = data.get("test_config", {})
|
||||
pq_result.global_data = data.get("global_data", {})
|
||||
|
||||
# 恢复测试项
|
||||
test_items_data = data.get("test_items", {})
|
||||
for item_name, item_data in test_items_data.items():
|
||||
pq_result.test_items[item_name] = TestItemResult.from_dict(item_data)
|
||||
|
||||
return pq_result
|
||||
|
||||
def export_item_data(self, item_name: str, export_format: str = "json") -> str:
|
||||
"""
|
||||
导出单个测试项的数据
|
||||
|
||||
Args:
|
||||
item_name: 测试项名称
|
||||
export_format: 导出格式 ("json", "csv")
|
||||
|
||||
Returns:
|
||||
导出文件路径
|
||||
"""
|
||||
if item_name not in self.test_items:
|
||||
raise ValueError(f"测试项 {item_name} 不存在")
|
||||
|
||||
item = self.test_items[item_name]
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if export_format == "json":
|
||||
filename = f"{self.test_id}_{item_name}_{timestamp}.json"
|
||||
if self.test_type:
|
||||
file_path = os.path.join(self.output_dir, self.test_type, filename)
|
||||
else:
|
||||
file_path = os.path.join(self.output_dir, filename)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(item.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
elif export_format == "csv":
|
||||
import csv
|
||||
|
||||
filename = f"{self.test_id}_{item_name}_{timestamp}.csv"
|
||||
if self.test_type:
|
||||
file_path = os.path.join(self.output_dir, self.test_type, filename)
|
||||
else:
|
||||
file_path = os.path.join(self.output_dir, filename)
|
||||
|
||||
with open(file_path, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# 写入基本信息
|
||||
writer.writerow(["测试项", item.item_display_name])
|
||||
writer.writerow(["状态", item.status])
|
||||
writer.writerow(
|
||||
["开始时间", item.start_time.isoformat() if item.start_time else ""]
|
||||
)
|
||||
writer.writerow(
|
||||
["结束时间", item.end_time.isoformat() if item.end_time else ""]
|
||||
)
|
||||
writer.writerow([])
|
||||
|
||||
# 写入最终结果数据
|
||||
writer.writerow(["最终结果"])
|
||||
for key, value in item.final_result.items():
|
||||
writer.writerow([key, str(value)])
|
||||
|
||||
else:
|
||||
raise ValueError(f"不支持的导出格式: {export_format}")
|
||||
|
||||
return file_path
|
||||
|
||||
def get_progress_info(self) -> Dict[str, Any]:
|
||||
"""获取测试进度信息"""
|
||||
total_items = len(self.test_items)
|
||||
completed_items = len(
|
||||
[
|
||||
item
|
||||
for item in self.test_items.values()
|
||||
if item.status in ["completed", "failed"]
|
||||
]
|
||||
)
|
||||
running_items = len(
|
||||
[item for item in self.test_items.values() if item.status == "running"]
|
||||
)
|
||||
|
||||
progress_percentage = (
|
||||
(completed_items / total_items * 100) if total_items > 0 else 0
|
||||
)
|
||||
|
||||
return {
|
||||
"total_items": total_items,
|
||||
"completed_items": completed_items,
|
||||
"running_items": running_items,
|
||||
"pending_items": total_items - completed_items - running_items,
|
||||
"progress_percentage": progress_percentage,
|
||||
"current_status": self.status,
|
||||
}
|
||||
|
||||
|
||||
# 使用示例和工具函数
|
||||
def create_pq_result_from_config(config: Dict[str, Any]) -> PQResult:
|
||||
"""根据配置创建PQResult实例"""
|
||||
test_type = config.get("test_type", "")
|
||||
test_name = config.get("test_name", "")
|
||||
|
||||
pq_result = PQResult(test_type=test_type, test_name=test_name)
|
||||
pq_result.set_test_config(config)
|
||||
|
||||
# 添加测试项
|
||||
test_items = config.get("test_items", [])
|
||||
test_items_names = config.get("test_items_chinese", [])
|
||||
|
||||
for i, item in enumerate(test_items):
|
||||
display_name = test_items_names[i] if i < len(test_items_names) else item
|
||||
pq_result.add_test_item(item, display_name)
|
||||
|
||||
return pq_result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("PQResult类测试")
|
||||
|
||||
# 创建测试实例
|
||||
pq_result = PQResult("screen_module", "屏模组性能测试")
|
||||
|
||||
# 设置配置
|
||||
config = {
|
||||
"test_type": "screen_module",
|
||||
"test_name": "屏模组性能测试",
|
||||
"test_items": ["gamut", "gamma", "cct"],
|
||||
"test_items_chinese": ["色域", "Gamma", "色温一致性"],
|
||||
}
|
||||
pq_result.set_test_config(config)
|
||||
|
||||
# 添加测试项
|
||||
pq_result.add_test_item("gamut", "色域")
|
||||
pq_result.add_test_item("gamma", "Gamma")
|
||||
pq_result.add_test_item("cct", "色温一致性")
|
||||
|
||||
# 模拟测试过程
|
||||
pq_result.start_test_item("gamut")
|
||||
pq_result.add_intermediate_data(
|
||||
"gamut", "measurement_points", [[0.64, 0.33], [0.30, 0.60]]
|
||||
)
|
||||
pq_result.set_test_item_result("gamut", {"coverage": 95.2, "accuracy": 98.5})
|
||||
|
||||
# 完成测试
|
||||
pq_result.complete_test()
|
||||
|
||||
print(f"测试结果已保存到: {pq_result.save_to_json()}")
|
||||
print("测试摘要:", pq_result.get_test_summary())
|
||||
423
utils/tvSerail.py
Normal file
423
utils/tvSerail.py
Normal file
@@ -0,0 +1,423 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
import zlib
|
||||
from xmlrpc.client import Boolean
|
||||
from utils.baseSerail import BaseSerial
|
||||
import binascii
|
||||
import utils.baseSerail as baseSerail
|
||||
|
||||
# 包头码(包引导码)
|
||||
PHeader = {
|
||||
"TV_Debug": 0xAA,
|
||||
"TV_Return": 0xAB,
|
||||
"TV_Panel_Debug": 0xAC,
|
||||
"TV_Panel_Return": 0xAD,
|
||||
"TV_Debug_Other": 0xAE,
|
||||
"TV_Other_Return": 0xAF,
|
||||
}
|
||||
|
||||
# 命令结果码;
|
||||
RCode = {
|
||||
"RC_OK": 0x0A, # 命令执行通过;
|
||||
"RC_ERR": 0x0E, # 命令错误或无法执行;
|
||||
"RC_LOSE": 0x0F, # 命令丢失(链路层出错);
|
||||
}
|
||||
|
||||
|
||||
# 命令的封装与解析;
|
||||
class TvParse():
|
||||
def __init__(self):
|
||||
# 头引导码(默认1字节,0x86命令二字节);
|
||||
self.Header = [PHeader['TV_Debug']]
|
||||
# 包长(默认1字节,0x86长度二字节);
|
||||
self.Length = [0x00]
|
||||
# 命令码,1字节(int类型);
|
||||
self.Command = [0x00]
|
||||
# 子命令参数;
|
||||
self.SubCommand = []
|
||||
# 数据,bytearray格式;
|
||||
self.Data = []
|
||||
# crch,1字节;
|
||||
self.CRCH = 0xFF
|
||||
# crcl,1字节;
|
||||
self.CRCL = 0xFF
|
||||
# 是否是特殊命令;
|
||||
self.FEFlag = False
|
||||
# 是否是多参数命令;(同一个Command,有多个含义的参数)
|
||||
self.isMultipleParams = False
|
||||
# 正确执行后的结果值,bytearray格式的二维数组;
|
||||
self.successData = []
|
||||
|
||||
def parseCommand(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), isMultipleParams=False, FEFlag=False):
|
||||
self.Header[0] = head
|
||||
self.Command = command
|
||||
self.SubCommand = subCommand
|
||||
self.Data = data
|
||||
self.FEFlag = FEFlag
|
||||
self.isMultipleParams = isMultipleParams
|
||||
if FEFlag:
|
||||
self.Header.append(0xFE)
|
||||
# 注意:4 = crch + crcl + lenh + lenl
|
||||
length = 4 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
|
||||
# 高字节;
|
||||
self.Length[0] = length >> 8
|
||||
# 低字节;
|
||||
self.Length.append(length & 0xFF)
|
||||
else:
|
||||
# 注意:3 = crch + crcl + len
|
||||
self.Length[0] = 3 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
|
||||
|
||||
# 生成package;
|
||||
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data
|
||||
|
||||
self.calculateCRC(package)
|
||||
|
||||
# 形成最终的命令;
|
||||
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data + bytearray([self.CRCH, self.CRCL])
|
||||
|
||||
# 打印最终命令的十六进制形式,每两个字符之间加一个空格
|
||||
# hex_string = ' '.join(f'{byte:02X}' for byte in package)
|
||||
# print("最终命令的十六进制形式:", hex_string)
|
||||
return package
|
||||
|
||||
def calculateCRC(self, data: bytearray):
|
||||
crc = baseSerail.crc16(data, data.__len__())
|
||||
# 高字节;
|
||||
self.CRCH = crc >> 8
|
||||
# 低字节;
|
||||
self.CRCL = crc & 0xFF
|
||||
|
||||
def parseCommandFc(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), isMultipleParams=False):
|
||||
self.Header[0] = head
|
||||
self.Command = command
|
||||
self.SubCommand = subCommand
|
||||
self.Data = data
|
||||
self.isMultipleParams = isMultipleParams
|
||||
self.Length[0] = 3 + self.Header.__len__() + self.Data.__len__() + self.Command.__len__() + self.SubCommand.__len__()
|
||||
|
||||
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data
|
||||
self.calculateCRC(package)
|
||||
package = bytearray(self.Header + self.Length + self.Command + self.SubCommand) + self.Data + bytearray([self.CRCH]) + bytearray([self.CRCL])
|
||||
return package
|
||||
|
||||
# 成功返回:(AB 08 FC 08 EC 80 CR1 CR2)
|
||||
# 失败返回:(AB 08 FC 08 EC 00 CR1 CR2)
|
||||
def parseResultFc(self,data):
|
||||
data = bytearray(data)
|
||||
|
||||
return False
|
||||
|
||||
def parseResult(self, data):
|
||||
data = bytearray(data)
|
||||
if data.__len__() < 5:
|
||||
return False
|
||||
|
||||
retCode = 0
|
||||
if self.Header[0] == PHeader['TV_Debug']:
|
||||
retCode = PHeader['TV_Return']
|
||||
elif self.Header[0] == PHeader['TV_Panel_Debug']:
|
||||
retCode = PHeader['TV_Panel_Return']
|
||||
elif self.Header[0] == PHeader['TV_Debug_Other']:
|
||||
retCode = PHeader['TV_Other_Return']
|
||||
|
||||
if retCode != data[0]:
|
||||
return False
|
||||
|
||||
package = []
|
||||
tooken_len = 0
|
||||
return True
|
||||
|
||||
def parseString(self, data):
|
||||
data = bytearray(data)
|
||||
if data.__len__() < 5:
|
||||
return False
|
||||
|
||||
retCode = 0
|
||||
if self.Header[0] == PHeader['TV_Debug']:
|
||||
retCode = PHeader['TV_Return']
|
||||
elif self.Header[0] == PHeader['TV_Panel_Debug']:
|
||||
retCode = PHeader['TV_Panel_Return']
|
||||
elif self.Header[0] == PHeader['TV_Debug_Other']:
|
||||
retCode = PHeader['TV_Other_Return']
|
||||
|
||||
if retCode != data[0]:
|
||||
return False
|
||||
|
||||
package = []
|
||||
tooken_len = 0
|
||||
while True:
|
||||
if tooken_len >= data.__len__():
|
||||
break
|
||||
if self.FEFlag:
|
||||
package_len = data[tooken_len + 1] << 8 + data[tooken_len + 2]
|
||||
else:
|
||||
package_len = data[tooken_len + 1]
|
||||
package = data[tooken_len:tooken_len + package_len]
|
||||
|
||||
if package[0] != retCode:
|
||||
print('Incorrect package head!\n')
|
||||
return False
|
||||
|
||||
crc = crc16(package, package_len - 2)
|
||||
CRCH = crc >> 8
|
||||
CRCL = crc & 0xFF
|
||||
if CRCH != package[-2] and CRCL != package[-1]:
|
||||
return False
|
||||
|
||||
if tooken_len == 0:
|
||||
if package[2] != RCode['RC_OK']:
|
||||
return False
|
||||
else:
|
||||
if self.Command[0] == 0xFC:
|
||||
pass
|
||||
else:
|
||||
if package[2] - 1 != self.Command[0]:
|
||||
return False
|
||||
if package[2] == 0xFE:
|
||||
self.successData.append(package[5:-2])
|
||||
else:
|
||||
self.successData.append(package[3:-2])
|
||||
|
||||
tooken_len += package_len
|
||||
|
||||
# print('successData:', binascii.b2a_hex(bytearray(self.Command)), self.successData)
|
||||
return self.successData
|
||||
|
||||
|
||||
class tvSerial(BaseSerial):
|
||||
def __init__(self):
|
||||
BaseSerial.__init__(self)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def checkport(self):
|
||||
if not self.ser.is_open:
|
||||
self.reOpen()
|
||||
|
||||
return self.ser.is_open
|
||||
|
||||
def sendcmd(self, cmd: list[int]):
|
||||
self.write(bytearray(cmd))
|
||||
return self.read()
|
||||
|
||||
'''协议模式发送命令'''
|
||||
def sendcmdEx(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
return cmd.parseResult(package)
|
||||
|
||||
return False
|
||||
|
||||
def sendcmdEx_string_return(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
return cmd.parseString(package)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def sendcmdFc(self, head: int, command: list[int], subCommand: list[int] = [], data: bytearray = bytearray(), FEFlag: Boolean = False, returnParam: Boolean = False):
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommand(head, command, subCommand, data, returnParam, FEFlag)
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
return cmd.parseResult(package)
|
||||
|
||||
return False
|
||||
|
||||
def gen_zzip(selfe,gm_file,save_file):
|
||||
with open(gm_file, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
compressed_data = zlib.compress(data)
|
||||
|
||||
with open(save_file, 'wb') as f:
|
||||
f.write(compressed_data)
|
||||
|
||||
'''发送pattern'''
|
||||
def send_parttern(self, rgb: list[int]):
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommand(0xAA, [0x28], [], bytearray(rgb))
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
return cmd.parseResult(package)
|
||||
|
||||
return False
|
||||
|
||||
# '''发送10bit-pattern'''
|
||||
# def send_10_bit_parttern(self, rgb: list[int]):
|
||||
# cmd = MokaParse()
|
||||
# rgb16 = convert_rgb16(rgb)
|
||||
# package = cmd.parseCommandFc(0xAA, [0xFC,0x07,0x06], [0xEC,0x01,0x86,0x00], rgb16)
|
||||
# if self.write(package):
|
||||
# package = self.read()
|
||||
# return cmd.parseResult(package)
|
||||
|
||||
# return False
|
||||
|
||||
'''发送12bit-pattern'''
|
||||
def send_12_bit_parttern(self, rgb: list[int]):
|
||||
cmd = TvParse()
|
||||
rgb16 = baseSerail.convert_rgb16(rgb)
|
||||
package = cmd.parseCommand(0xAA, [0x28], [], rgb16)
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
return cmd.parseResult(package)
|
||||
|
||||
return False
|
||||
|
||||
'''发送gamma文件'''
|
||||
def send_gamma(self, file_path: str):
|
||||
fp = open(file_path, 'rb')
|
||||
cmd = TvParse()
|
||||
data = fp.read()
|
||||
# 生成命令包
|
||||
package = cmd.parseCommand(0xAA, [0xE9], [0x02], data, False, True)
|
||||
# 将命令包写入临时文件
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
print(cmd.parseResult(package))
|
||||
return cmd.parseResult(package)
|
||||
|
||||
'''激活gamma文件'''
|
||||
def send_gamma_active(self, ini_file: str):
|
||||
ini_fp = open(ini_file, 'rb')
|
||||
ind_data = ini_fp.read()
|
||||
ini_fp.close()
|
||||
|
||||
# 计算crc
|
||||
crc = crc16(ind_data, ind_data.__len__())
|
||||
CRC_GM = [crc >> 8, crc & 0xFF]
|
||||
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommand(0xAA, [0x99], [0x06], bytearray(CRC_GM))
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
print("---------------------")
|
||||
print(cmd.parseResult(package))
|
||||
return cmd.parseResult(package)
|
||||
|
||||
'''激活gamma文件-12bit版本'''
|
||||
def send_gamma_active_12bit(self, ini_file: str):
|
||||
ini_fp = open(ini_file, 'rb')
|
||||
ind_data = ini_fp.read()
|
||||
ini_fp.close()
|
||||
|
||||
# 计算crc
|
||||
crc = baseSerail.crc16(ind_data, ind_data.__len__())
|
||||
CRC_GM = [crc >> 8, crc & 0xFF]
|
||||
|
||||
cmd = TvParse()
|
||||
package = cmd.parseCommandFc(0xAA, [0xFC,0x07], [0x0E], bytearray(CRC_GM))
|
||||
if self.write(package):
|
||||
package = self.read()
|
||||
print("---------------------")
|
||||
print(cmd.parseResult(package))
|
||||
return cmd.parseResult(package)
|
||||
|
||||
|
||||
'''进工厂模式'''
|
||||
def enterFactory(self):
|
||||
return self.sendcmdEx(0xAA, [0x10], [0x01])
|
||||
|
||||
'''白平衡初始化'''
|
||||
def initWhiteBalance(self):
|
||||
return self.sendcmdEx(0xAA, [0x16], [0x01])
|
||||
|
||||
'''关闭localdimming'''
|
||||
def closeLocaldimming(self):
|
||||
return self.sendcmdEx(0xAA, [0x9F,0x07], [0x00])
|
||||
|
||||
'''打开内置pattern'''
|
||||
def openBuiltInPattern(self):
|
||||
return self.sendcmdEx(0xAA, [0x27], [0x01])
|
||||
|
||||
'''关闭内置pattern'''
|
||||
def closeBuiltInPattern(self):
|
||||
return self.sendcmdEx(0xAA, [0x27], [0x00])
|
||||
|
||||
'''切换标准色温'''
|
||||
def switchStdColorTemperature(self):
|
||||
return self.sendcmdEx(0xAA, [0x31], [0x01])
|
||||
|
||||
'''切换冷色温'''
|
||||
def switchColdColorTemperature(self):
|
||||
return self.sendcmdEx(0xAA, [0x31], [0x02])
|
||||
|
||||
'''切换暖色温'''
|
||||
def switchWarmColorTemperature(self):
|
||||
return self.sendcmdEx(0xAA, [0x31], [0x03])
|
||||
|
||||
'''切换暖2色温'''
|
||||
def switchWarm2ColorTemperature(self):
|
||||
return self.sendcmdEx(0xAA, [0x31], [0x04])
|
||||
|
||||
'''初始化gamma'''
|
||||
def initGamma(self):
|
||||
return self.sendcmdEx(0xAA, [0x9F,0x09], [0x01])
|
||||
|
||||
'''进老化模式'''
|
||||
def enterAgingMode(self):
|
||||
return self.sendcmdEx(0xAA, [0x13], [0x01])
|
||||
|
||||
'''退出老化模式'''
|
||||
def exitAgingMode(self):
|
||||
return self.sendcmdEx(0xAA, [0x13], [0x00])
|
||||
|
||||
'''软件版本查询'''
|
||||
def sendSoftwareVersionQuery(self):
|
||||
return self.sendcmdEx_string_return(0xAA, [0x57], [0x00])
|
||||
|
||||
'''PID查询'''
|
||||
def sendPIDQuery(self):
|
||||
return self.sendcmdEx_string_return(0xAA, [0x84], [0x00])
|
||||
|
||||
def switchHDMI1(self):
|
||||
"""切换到HDMI1信源"""
|
||||
return self.sendcmdEx(0xAA, [0x25], [0x01])
|
||||
|
||||
def switchHDMI2(self):
|
||||
"""切换到HDMI2信源"""
|
||||
return self.sendcmdEx(0xAA, [0x25], [0x02])
|
||||
|
||||
def switchHDMI3(self):
|
||||
"""切换到HDMI3信源"""
|
||||
return self.sendcmdEx(0xAA, [0x25], [0x03])
|
||||
|
||||
def switchVGA(self):
|
||||
"""切换到VGA信源"""
|
||||
return self.sendcmdEx(0xAA, [0x24], [0x01])
|
||||
|
||||
def switchAV1(self):
|
||||
"""切换到AV1信源"""
|
||||
return self.sendcmdEx(0xAA, [0x22], [0x01])
|
||||
|
||||
def switchAV2(self):
|
||||
"""切换到AV2信源"""
|
||||
return self.sendcmdEx(0xAA, [0x22], [0x02])
|
||||
|
||||
def switchAV3(self):
|
||||
"""切换到AV3信源"""
|
||||
return self.sendcmdEx(0xAA, [0x22], [0x03])
|
||||
|
||||
def switchDisplayMode(self, mode="natural"):
|
||||
"""切换图像预设模式 """
|
||||
if mode == "natural":
|
||||
return self.sendcmdEx(0xAA, [0x30], [0x01])
|
||||
elif mode == "soft":
|
||||
return self.sendcmdEx(0xAA, [0x30], [0x02])
|
||||
elif mode == "bright":
|
||||
return self.sendcmdEx(0xAA, [0x30], [0x03])
|
||||
elif mode == "personal":
|
||||
return self.sendcmdEx(0xAA, [0x30], [0x04])
|
||||
elif mode == "cinematic":
|
||||
return self.sendcmdEx(0xAA, [0x30], [0x05])
|
||||
else:
|
||||
raise ValueError("Invalid display mode. Use 'natural', 'soft', 'bright', 'personal', or 'cinematic'.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
Reference in New Issue
Block a user