重构移动utils文件夹

This commit is contained in:
xinzhu.yin
2026-04-20 11:48:38 +08:00
parent b6c1c2ab93
commit 2e92b48496
27 changed files with 2866 additions and 3085 deletions

288
app/data_range_converter.py Normal file
View File

@@ -0,0 +1,288 @@
# -*- coding: UTF-8 -*-
"""
数据范围转换器
将 Full Range (0-255) 转换为 Limited Range (16-235)
使用方法:
from app.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 app.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 app.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)

View File

@@ -8,7 +8,7 @@ import threading
import time
from tkinter import messagebox
from utils.caSerail import CASerail
from drivers.caSerail import CASerail
def get_available_ucd_ports(self):
"""获取可用的UCD端口列表"""

0
app/pq/__init__.py Normal file
View File

601
app/pq/pq_config.py Normal file
View 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
app/pq/pq_result.py Normal file
View 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())

View File

@@ -14,8 +14,8 @@ import colour
import numpy as np
import algorithm.pq_algorithm as pq_algorithm
from utils.data_range_converter import convert_pattern_params
from utils.pq.pq_result import PQResult
from app.data_range_converter import convert_pattern_params
from app.pq.pq_result import PQResult
def new_pq_results(self, test_type, test_name):
self.results = PQResult(test_type, test_name)

View File

@@ -1,127 +1,214 @@
"""Local Dimming 测试逻辑(Step 4 重构)。
"""Local Dimming 测试逻辑(应用层)。
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
以保留原有 `self.xxx` 属性访问不变
整合自原 drivers/local_dimming_test.py窗口图片生成与测试主循环
直接落在本模块UCD 通用操作下沉到 drivers.ucd_helpers
"""
import atexit
import csv
import datetime
import os
import shutil
import sys
import threading
import time
import tkinter as tk
from tkinter import filedialog, messagebox
import numpy as np
from PIL import Image
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
# --------------------------------------------------------------------------
# 模块级常量与窗口图片缓存
# --------------------------------------------------------------------------
DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
_TEMP_DIR = None
_IMAGE_CACHE = {} # {(width, height, percentage): file_path}
def _cleanup_temp_dir():
global _TEMP_DIR
if _TEMP_DIR and os.path.exists(_TEMP_DIR):
try:
shutil.rmtree(_TEMP_DIR)
except Exception:
pass
_TEMP_DIR = None
_IMAGE_CACHE.clear()
def _get_temp_dir():
global _TEMP_DIR
if _TEMP_DIR is None:
if getattr(sys, "frozen", False):
base = os.path.dirname(sys.executable)
else:
base = os.getcwd()
_TEMP_DIR = os.path.join(base, "temp_local_dimming")
os.makedirs(_TEMP_DIR, exist_ok=True)
atexit.register(_cleanup_temp_dir)
return _TEMP_DIR
def _make_window_image_array(width, height, percentage):
"""生成黑底+居中白窗的 numpy 图像,保持屏幕比例。"""
image = np.zeros((height, width, 3), dtype=np.uint8)
if percentage >= 100:
ww, wh = width, height
else:
scale = (percentage / 100.0) ** 0.5
ww = int(width * scale)
wh = int(height * scale)
x1 = (width - ww) // 2
y1 = (height - wh) // 2
image[y1:y1 + wh, x1:x1 + ww] = 255
return image
def _ensure_window_image(width, height, percentage):
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
key = (width, height, percentage)
cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached):
return cached
arr = _make_window_image_array(width, height, percentage)
fname = f"window_{width}x{height}_{percentage:03d}percent.png"
path = os.path.join(_get_temp_dir(), fname)
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path
return path
# --------------------------------------------------------------------------
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def start_local_dimming_test(self):
"""开始 Local Dimming 测试"""
# 检查设备连接
"""开始 Local Dimming 测试"""
if not self.ca or not self.ucd.status:
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
return
# 禁用按钮
self.ld_start_btn.config(state=tk.DISABLED)
self.ld_stop_btn.config(state=tk.NORMAL)
self.ld_save_btn.config(state=tk.DISABLED)
# 清空结果
for item in self.ld_tree.get_children():
self.ld_tree.delete(item)
# 获取配置
wait_time = float(self.ld_wait_time_var.get())
stop_event = threading.Event()
self.ld_stop_event = stop_event
# 在新线程中执行测试
def run_test():
from utils.local_dimming_test import LocalDimmingTest, LocalDimmingController
def worker():
log = self.log_gui.log
log("=" * 60)
log("开始 Local Dimming 测试")
log("=" * 60)
# 从设备当前 timing 获取分辨率
ld_ctrl = LocalDimmingController(self.ucd)
cur_w, cur_h = ld_ctrl.get_current_resolution()
resolution = f"{cur_w}x{cur_h}"
width, height = get_current_resolution(self.ucd)
total = len(DEFAULT_WINDOW_PERCENTAGES)
log(f" 分辨率: {width}x{height}")
log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}")
log(f" 等待时间: {wait_time}")
ld_test = LocalDimmingTest(
self.ucd,
self.ca,
log_callback=self.log_gui.log,
)
results = []
for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
if stop_event.is_set():
log("⚠️ 测试已停止")
break
ld_test.wait_time = wait_time
log(f"[{i}/{total}] 测试 {percentage}% 窗口...")
try:
image_path = _ensure_window_image(width, height, percentage)
except Exception as e:
log(f" ❌ 图像生成失败: {e}")
continue
results = ld_test.run_test(resolution=resolution)
if not send_image_pattern(self.ucd, image_path):
log(f"{percentage}% 窗口发送失败,跳过")
continue
log(f" ⏳ 等待 {wait_time} 秒...")
time.sleep(wait_time)
try:
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
except Exception as e:
log(f" ❌ 采集亮度异常: {e}")
continue
if lv is None:
log(f"{percentage}% 窗口采集失败")
continue
log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
results.append((percentage, x, y, lv, _X, _Y, _Z))
log("=" * 60)
log(f"✅ Local Dimming 测试完成 ({len(results)}/{total})")
log("=" * 60)
# 保存到实例变量
self.ld_test_instance = ld_test
self.ld_test_results = results
# 更新结果显示
self.root.after(0, lambda: self.update_ld_results(results))
# 清理临时文件
ld_test.cleanup()
# 恢复按钮状态
self.root.after(0, lambda: self.ld_start_btn.config(state=tk.NORMAL))
self.root.after(0, lambda: self.ld_stop_btn.config(state=tk.DISABLED))
self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL))
threading.Thread(target=run_test, daemon=True).start()
threading.Thread(target=worker, daemon=True).start()
def update_ld_results(self, results):
"""更新 Local Dimming 结果显示"""
for percentage, x, y, lv, X, Y, Z in results:
"""把批量测试结果填入 Treeview。"""
for percentage, x, y, lv, _X, _Y, _Z in results:
self.ld_tree.insert(
"",
tk.END,
"", tk.END,
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
)
def stop_local_dimming_test(self):
"""停止 Local Dimming 测试"""
if hasattr(self, "ld_test_instance"):
self.ld_test_instance.stop()
"""请求停止当前 Local Dimming 测试"""
ev = getattr(self, "ld_stop_event", None)
if ev:
ev.set()
def send_ld_window(self, percentage):
"""发送指定百分比的窗口"""
"""发送指定百分比的白色窗口(手动模式)。"""
if not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
# 记录当前百分比(用于测量)
self.current_ld_percentage = percentage
def send():
from utils.local_dimming_test import LocalDimmingController
ld_controller = LocalDimmingController(self.ucd)
# 从设备当前 timing 获取分辨率
width, height = ld_controller.get_current_resolution()
# 生成并发送图片
success = ld_controller.send_window_pattern_with_resolution(
percentage, width, height
width, height = get_current_resolution(self.ucd)
try:
image_path = _ensure_window_image(width, height, percentage)
except Exception as e:
self.root.after(0, lambda: self.log_gui.log(f"❌ 图像生成失败: {e}"))
return
ok = send_image_pattern(self.ucd, image_path)
msg = (
f"{percentage}% 窗口已发送" if ok
else f"{percentage}% 窗口发送失败"
)
if success:
self.root.after(
0, lambda: self.log_gui.log(f"{percentage}% 窗口已发送")
)
else:
self.root.after(
0, lambda: self.log_gui.log(f"{percentage}% 窗口发送失败")
)
self.root.after(0, lambda: self.log_gui.log(msg))
threading.Thread(target=send, daemon=True).start()
def measure_ld_luminance(self):
"""测量当前亮度"""
"""测量当前显示的亮度并追加一行到 Treeview。"""
if not self.ca:
messagebox.showwarning("警告", "请先连接 CA410 色度计")
return
if self.current_ld_percentage is None:
messagebox.showinfo("提示", "请先发送一个窗口图案")
return
@@ -130,51 +217,31 @@ def measure_ld_luminance(self):
def measure():
try:
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
if lv is not None:
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
# 更新显示
self.root.after(
0,
lambda: self.ld_result_label.config(
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}"
),
)
# 添加到表格
self.root.after(
0,
lambda: self.ld_tree.insert(
"",
tk.END,
values=(
f"{self.current_ld_percentage}%",
f"{lv:.2f}",
f"{x:.4f}",
f"{y:.4f}",
timestamp,
),
),
)
self.root.after(
0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²")
)
else:
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
except Exception as e:
self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}"))
return
if lv is None:
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
return
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self.root.after(0, lambda: self.ld_result_label.config(
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}"
))
self.root.after(0, lambda: self.ld_tree.insert(
"", tk.END,
values=(
f"{self.current_ld_percentage}%",
f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp,
),
))
self.root.after(0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²"))
threading.Thread(target=measure, daemon=True).start()
def clear_ld_records(self):
"""清空测试记录"""
"""清空 Treeview 中的测试记录"""
for item in self.ld_tree.get_children():
self.ld_tree.delete(item)
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
@@ -183,24 +250,20 @@ def clear_ld_records(self):
def save_local_dimming_results(self):
"""保存 Local Dimming 结果"""
from tkinter import filedialog
import csv
import datetime
"""把 Treeview 中的全部记录导出为 CSV。"""
if len(self.ld_tree.get_children()) == 0:
messagebox.showinfo("提示", "没有可保存的数据")
return
default_name = f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
default_name = (
f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
save_path = filedialog.asksaveasfilename(
title="保存测试结果",
initialfile=default_name,
defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
)
if not save_path:
return
@@ -208,14 +271,10 @@ def save_local_dimming_results(self):
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
for item in self.ld_tree.get_children():
values = self.ld_tree.item(item, "values")
writer.writerow(values)
writer.writerow(self.ld_tree.item(item, "values"))
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
except Exception as e:
self.log_gui.log(f"❌ 保存失败: {str(e)}")
messagebox.showerror("错误", f"保存失败: {str(e)}")

View File

@@ -8,7 +8,7 @@ import tkinter as tk
import ttkbootstrap as ttk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from views.pq_debug_panel import PQDebugPanel
from app.views.pq_debug_panel import PQDebugPanel
def init_gamut_chart(self):
"""初始化色域图表 - 手动设置subplot位置完全避免重叠"""

View File

@@ -0,0 +1,127 @@
import ttkbootstrap as ttk
import tkinter
from tkinter import ttk
from pathlib import Path
from ttkbootstrap import Style
import sys
import os
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径(兼容开发环境和打包后)
Args:
relative_path: 相对路径,如 "assets/icons8_double_up_24px.png"
Returns:
str: 资源文件的绝对路径
"""
try:
# PyInstaller 打包后的临时文件夹路径
base_path = sys._MEIPASS
except AttributeError:
# 开发环境:使用项目根目录
# 当前文件: app/views/collapsing_frame.py
# 项目根目录: app/views 的祖父目录
current_file = os.path.abspath(__file__)
views_dir = os.path.dirname(current_file)
app_dir = os.path.dirname(views_dir)
base_path = os.path.dirname(app_dir)
return os.path.join(base_path, relative_path)
class CollapsingFrame(ttk.Frame):
"""
A collapsible frame widget that opens and closes with a button click.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.columnconfigure(0, weight=1)
self.cumulative_rows = 0
p = Path(__file__).parent
self.images = [
tkinter.PhotoImage(
name="open", file=get_resource_path("assets/icons8_double_up_24px.png")
),
tkinter.PhotoImage(
name="closed",
file=get_resource_path("assets/icons8_double_right_24px.png"),
),
]
def add(self, child, title="", style="primary.TButton", **kwargs):
"""Add a child to the collapsible frame
:param ttk.Frame child: the child frame to add to the widget
:param str title: the title appearing on the collapsible section header
:param str style: the ttk style to apply to the collapsible section header
"""
if child.winfo_class() != "TFrame": # must be a frame
return
style_color = style.split(".")[0]
frm = ttk.Frame(self, style=f"{style_color}.TFrame")
frm.grid(row=self.cumulative_rows, column=0, sticky="ew")
# header title
lbl = ttk.Label(frm, text=title, style=f"{style_color}.Inverse.TLabel")
if kwargs.get("textvariable"):
lbl.configure(textvariable=kwargs.get("textvariable"))
lbl.pack(side="left", fill="both", padx=10)
# header toggle button
btn = ttk.Button(
frm,
image="open",
style=style,
command=lambda c=child: self._toggle_open_close(child),
)
btn.pack(side="right")
# assign toggle button to child so that it's accesible when toggling (need to change image)
child.btn = btn
child.grid(row=self.cumulative_rows + 1, column=0, sticky="news")
# increment the row assignment
self.cumulative_rows += 2
def _toggle_open_close(self, child):
"""
Open or close the section and change the toggle button image accordingly
:param ttk.Frame child: the child element to add or remove from grid manager
"""
if child.winfo_viewable():
child.grid_remove()
child.btn.configure(image="closed")
else:
child.grid()
child.btn.configure(image="open")
# class Application(tkinter.Tk):
# def __init__(self):
# super().__init__()
# self.title('Collapsing Frame')
# # self.style = Style()
# cf = CollapsingFrame(self)
# cf.pack(fill='both')
# # option group 1
# group1 = ttk.Frame(cf, padding=10)
# for x in range(5):
# ttk.Checkbutton(group1, text=f'Option {x + 1}').pack(fill='x')
# cf.add(group1, title='Option Group 1', style='primary.TButton')
# # option group 2
# group2 = ttk.Frame(cf, padding=10)
# for x in range(5):
# ttk.Checkbutton(group2, text=f'Option {x + 1}').pack(fill='x')
# cf.add(group2, title='Option Group 2', style='danger.TButton')
# if __name__ == '__main__':
# Application().mainloop()

View File

@@ -0,0 +1,72 @@
"""面板管理器Step 6 重构)。
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
"""
import tkinter as tk
def register_panel(self, panel_name, frame, button, visible_attr):
"""注册一个面板到管理系统"""
self.panels[panel_name] = {
"frame": frame,
"button": button,
"visible_attr": visible_attr,
}
def show_panel(self, panel_name):
"""显示指定面板,隐藏其他所有面板"""
if panel_name not in self.panels:
return
# 如果当前面板就是要显示的面板,则隐藏它
if self.current_panel == panel_name:
self.hide_all_panels()
return
# 隐藏所有面板
self.hide_all_panels()
# 显示指定面板
panel_info = self.panels[panel_name]
# 隐藏主内容区域
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
# 显示目标面板
panel_info["frame"].pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
# 更新按钮样式
if panel_info["button"]:
panel_info["button"].configure(style="SidebarSelected.TButton")
# 更新状态
setattr(self, panel_info["visible_attr"], True)
self.current_panel = panel_name
def hide_all_panels(self):
"""隐藏所有面板,显示主内容区域"""
# 隐藏所有注册的面板
for panel_name, panel_info in self.panels.items():
panel_info["frame"].pack_forget()
if panel_info["button"]:
panel_info["button"].configure(style="Sidebar.TButton")
setattr(self, panel_info["visible_attr"], False)
# 显示主内容区域
self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_middle.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.current_panel = None

View File

View File

@@ -0,0 +1,901 @@
"""CCT 参数面板及其处理函数Step 6 重构)。"""
import time
import traceback
from tkinter import messagebox
import tkinter as tk
import ttkbootstrap as ttk
import algorithm.pq_algorithm as pq_algorithm
def create_cct_params_frame(self):
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)"""
# ==================== 屏模组色度参数 Frame ====================
self.cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置(屏模组)"
)
# 默认值
self.DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取屏模组参数
saved_params = self.config.current_test_types.get("screen_module", {}).get(
"cct_params", self.DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
"gamut_reference", "DCI-P3"
)
# 创建屏模组变量
self.cct_x_ideal_var = tk.StringVar(
value=str(saved_params.get("x_ideal", 0.3127))
)
self.cct_x_tolerance_var = tk.StringVar(
value=str(saved_params.get("x_tolerance", 0.003))
)
self.cct_y_ideal_var = tk.StringVar(
value=str(saved_params.get("y_ideal", 0.3290))
)
self.cct_y_tolerance_var = tk.StringVar(
value=str(saved_params.get("y_tolerance", 0.003))
)
self.screen_gamut_ref_var = tk.StringVar(value=saved_gamut_ref)
# 创建屏模组输入框(左侧:色度参数)
params = [
("x-ideal:", self.cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(params):
ttk.Label(self.cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
screen_gamut_combo = ttk.Combobox(
self.cct_params_frame,
textvariable=self.screen_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
screen_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
screen_gamut_combo.bind(
"<<ComboboxSelected>>", self.on_screen_gamut_ref_changed
)
self.screen_gamut_combo = screen_gamut_combo
# ==================== ✅ 单步调试按钮(右侧第二行)====================
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.screen_debug_btn = ttk.Button(
self.cct_params_frame,
text="打开调试面板",
command=self.toggle_screen_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.screen_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮(屏模组)
self.recalc_cct_btn = ttk.Button(
self.cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_cct_btn.grid_remove()
# 色域重新计算按钮
self.recalc_gamut_btn = ttk.Button(
self.cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== SDR 色度参数 Frame ====================
self.sdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置SDR"
)
# SDR 默认值
self.SDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 SDR 参数
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
"cct_params", self.SDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
"gamut_reference", "BT.709"
)
# 创建 SDR 变量
self.sdr_cct_x_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("x_ideal", 0.3127))
)
self.sdr_cct_x_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("x_tolerance", 0.003))
)
self.sdr_cct_y_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("y_ideal", 0.3290))
)
self.sdr_cct_y_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("y_tolerance", 0.003))
)
self.sdr_gamut_ref_var = tk.StringVar(value=sdr_saved_gamut_ref)
# 创建 SDR 输入框
sdr_params = [
("x-ideal:", self.sdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.sdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.sdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.sdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(sdr_params):
ttk.Label(self.sdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.sdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.SDR_DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.sdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
sdr_gamut_combo = ttk.Combobox(
self.sdr_cct_params_frame,
textvariable=self.sdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
sdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
self.sdr_gamut_combo = sdr_gamut_combo
# ==================== ✅ SDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.sdr_debug_btn = ttk.Button(
self.sdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_sdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.sdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮SDR
self.sdr_recalc_cct_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.sdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮SDR
self.sdr_recalc_gamut_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.sdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.sdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== HDR 色度参数 Frame ====================
self.hdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置HDR"
)
# HDR 默认值
self.HDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 HDR 参数
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
"cct_params", self.HDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
"gamut_reference", "BT.2020"
)
# 创建 HDR 变量
self.hdr_cct_x_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("x_ideal", 0.3127))
)
self.hdr_cct_x_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("x_tolerance", 0.003))
)
self.hdr_cct_y_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("y_ideal", 0.3290))
)
self.hdr_cct_y_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("y_tolerance", 0.003))
)
self.hdr_gamut_ref_var = tk.StringVar(value=hdr_saved_gamut_ref)
# 创建 HDR 输入框
hdr_params = [
("x-ideal:", self.hdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.hdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.hdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.hdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(hdr_params):
ttk.Label(self.hdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.hdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.HDR_DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.hdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
hdr_gamut_combo = ttk.Combobox(
self.hdr_cct_params_frame,
textvariable=self.hdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
hdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
self.hdr_gamut_combo = hdr_gamut_combo
# ==================== ✅ HDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.hdr_debug_btn = ttk.Button(
self.hdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_hdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.hdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮HDR
self.hdr_recalc_cct_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.hdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮HDR
self.hdr_recalc_gamut_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.hdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.hdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
def on_sdr_cct_param_focus_out(self, var, default_value):
"""SDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ SDR 参数为空,恢复默认值: {default_value}")
else:
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ SDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ SDR 参数无效,恢复默认值: {default_value}")
self.save_sdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 SDR 参数失败: {str(e)}")
def save_sdr_cct_params(self):
"""保存 SDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
sdr_cct_params = {
"x_ideal": get_float(
self.sdr_cct_x_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.sdr_cct_x_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.sdr_cct_y_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.sdr_cct_y_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "sdr_movie" not in self.config.current_test_types:
self.config.current_test_types["sdr_movie"] = {}
self.config.current_test_types["sdr_movie"]["cct_params"] = sdr_cct_params
self.save_pq_config()
except:
pass
def on_hdr_cct_param_focus_out(self, var, default_value):
"""HDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ HDR 参数为空,恢复默认值: {default_value}")
else:
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ HDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ HDR 参数无效,恢复默认值: {default_value}")
self.save_hdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 HDR 参数失败: {str(e)}")
def save_hdr_cct_params(self):
"""保存 HDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
hdr_cct_params = {
"x_ideal": get_float(
self.hdr_cct_x_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.hdr_cct_x_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.hdr_cct_y_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.hdr_cct_y_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "hdr_movie" not in self.config.current_test_types:
self.config.current_test_types["hdr_movie"] = {}
self.config.current_test_types["hdr_movie"]["cct_params"] = hdr_cct_params
self.save_pq_config()
except:
pass
def recalculate_cct(self):
"""重新计算并绘制色度图"""
try:
# 1. 保存新参数
self.save_cct_params()
self.log_gui.log("✓ 色度参数已更新")
# 2. 收起配置项
if hasattr(self, "config_panel_frame"):
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.1)
except:
pass
# 3. 跳转到色度图Tab
self.chart_notebook.select(self.cct_chart_frame)
self.root.update_idletasks()
# 4. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 5. 获取保存的灰阶数据
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data("cct", "gray")
if not gray_data or len(gray_data) < 2:
self.log_gui.log("⚠️ 没有可用的灰阶数据")
messagebox.showwarning("警告", "没有找到色度测试数据")
return
# 6. 重新计算 CCT
self.log_gui.log("=" * 50)
self.log_gui.log("开始重新计算色度一致性...")
self.log_gui.log("=" * 50)
import algorithm.pq_algorithm as pq_algorithm
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
# 7. 更新结果
self.results.set_test_item_result("cct", {"cct_values": cct_values})
# 8. 重新绘制色度图
test_type = self.config.current_test_type
self.plot_cct(test_type)
self.log_gui.log("✓ 色度图已重新绘制")
self.log_gui.log("=" * 50)
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
except Exception as e:
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def recalculate_gamut(self):
"""重新计算并绘制色域图(使用新的参考标准)"""
try:
# 1. 收起配置项
if hasattr(self, "config_panel_frame"):
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.1)
except:
pass
# 2. 跳转到色域图Tab
self.chart_notebook.select(self.gamut_chart_frame)
self.root.update_idletasks()
# 3. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 4. 获取保存的色域数据
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if not rgb_data or len(rgb_data) < 3:
self.log_gui.log("⚠️ 没有可用的色域数据")
messagebox.showwarning("警告", "没有找到色域测试数据")
return
# 5. 获取当前测试类型
test_type = self.config.current_test_type
# 6. 获取用户选择的参考标准
if test_type == "screen_module":
reference_standard = self.screen_gamut_ref_var.get()
elif test_type == "sdr_movie":
reference_standard = self.sdr_gamut_ref_var.get()
elif test_type == "hdr_movie":
reference_standard = self.hdr_gamut_ref_var.get()
else:
reference_standard = "DCI-P3"
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard}...")
self.log_gui.log("=" * 50)
# 7. 重新计算 XY 色域覆盖率
xy_points = [[result[0], result[1]] for result in rgb_data]
# 根据参考标准计算 XY 覆盖率
if reference_standard == "BT.2020":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
xy_points
)
elif reference_standard == "BT.709":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT709(
xy_points
)
elif reference_standard == "DCI-P3":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
else:
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
reference_standard = "DCI-P3"
self.log_gui.log(f"✓ 参考标准: {reference_standard}")
self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%")
# ========== ✅✅✅ 8. 重新计算 UV 色域覆盖率 ==========
# 将 XY 坐标转换为 UV 坐标
uv_points = []
for x, y in xy_points:
try:
# XY转UV公式
denom = -2 * x + 12 * y + 3
if abs(denom) < 1e-10:
u, v = 0, 0
else:
u = 4 * x / denom
v = 9 * y / denom
uv_points.append([u, v])
except ZeroDivisionError:
continue
self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}")
# 根据参考标准计算 UV 覆盖率
if reference_standard == "BT.2020":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
uv_points
)
elif reference_standard == "BT.709":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT709_uv(
uv_points
)
elif reference_standard == "DCI-P3":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
else:
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%")
# ========================================================
# 9. ✅ 更新结果(同时保存 XY 和 UV 覆盖率)
self.results.set_test_item_result(
"gamut",
{
"area": area_xy, # ← 兼容旧字段
"coverage": coverage_xy, # ← 兼容旧字段
"area_xy": area_xy, # ← XY 面积
"coverage_xy": coverage_xy, # ← XY 覆盖率
"area_uv": area_uv, # ← UV 面积
"coverage_uv": coverage_uv, # ← UV 覆盖率
"uv_coverage": coverage_uv, # ← 兼容字段Excel 导出用)
"reference": reference_standard,
},
)
self.log_gui.log("✓ 测试结果已更新到 results 对象")
# 10. 重新绘制色域图
self.plot_gamut(rgb_data, coverage_xy, test_type)
self.log_gui.log("✓ 色域图已重新绘制")
self.log_gui.log("=" * 50)
messagebox.showinfo(
"成功",
f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n"
f"XY 覆盖率: {coverage_xy:.1f}%\n"
f"UV 覆盖率: {coverage_uv:.1f}%",
)
except Exception as e:
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def on_cct_param_change(self, var, default_value):
"""色度参数改变时的处理 - 空值恢复默认"""
try:
value = var.get().strip()
if value == "":
# 空值:恢复默认值
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"输入框为空,恢复默认值: {default_value}")
else:
# 验证是否为有效数字
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"参数超出范围 [0, 1],恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}")
# 保存配置
self.save_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理参数变化失败: {str(e)}")
def on_cct_param_focus_out(self, var, default_value):
"""色度参数失去焦点时的处理 - 空值恢复默认"""
try:
value = var.get().strip()
if value == "":
# 空值:恢复默认值
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ 输入框为空,恢复默认值: {default_value}")
else:
# 验证是否为有效数字
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ 参数超出范围 [0, 1],恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ 无效的参数值,恢复默认值: {default_value}")
# 保存配置
self.save_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理参数变化失败: {str(e)}")
def save_cct_params(self):
"""保存色度参数 - 简化版"""
try:
current_type = self.config.current_test_type
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
cct_params = {
"x_ideal": get_float(
self.cct_x_ideal_var, self.DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.cct_x_tolerance_var, self.DEFAULT_CCT_PARAMS["x_tolerance"]
),
"y_ideal": get_float(
self.cct_y_ideal_var, self.DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.cct_y_tolerance_var, self.DEFAULT_CCT_PARAMS["y_tolerance"]
),
}
if current_type not in self.config.current_test_types:
self.config.current_test_types[current_type] = {}
self.config.current_test_types[current_type]["cct_params"] = cct_params
self.save_pq_config()
except:
pass
def reload_cct_params(self):
"""切换测试类型时重新加载色度参数"""
try:
current_type = self.config.current_test_type
saved_params = self.config.current_test_types.get(current_type, {}).get(
"cct_params", None
)
if saved_params is None:
saved_params = self.DEFAULT_CCT_PARAMS.copy()
# 更新输入框的值
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"]))
self.cct_y_ideal_var.set(str(saved_params["y_ideal"]))
self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"]))
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"重新加载色度参数失败: {str(e)}")
def toggle_cct_params_frame(self):
"""根据测试类型和测试项的选中状态显示对应参数框"""
selected_items = self.get_selected_test_items()
current_test_type = self.config.current_test_type
# ========== 默认隐藏所有参数框 ==========
self.cct_params_frame.pack_forget()
self.sdr_cct_params_frame.pack_forget()
# HDR 色度参数框(如果存在的话)
if hasattr(self, "hdr_cct_params_frame"):
self.hdr_cct_params_frame.pack_forget()
# ========== 根据测试类型和选中项显示对应参数框 ==========
if current_test_type == "screen_module":
# 屏模组:只有色度参数
if "cct" in selected_items:
self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示屏模组色度参数设置")
elif current_test_type == "sdr_movie":
# SDR只有色度参数色准不需要参数设置框
if "cct" in selected_items:
self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示 SDR 色度参数设置")
elif current_test_type == "hdr_movie":
# HDR只有色度参数色准不需要参数设置框
if "cct" in selected_items:
if hasattr(self, "hdr_cct_params_frame"):
self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示 HDR 色度参数设置")
else:
if hasattr(self, "log_gui"):
self.log_gui.log("⚠️ HDR 色度参数框尚未创建")

View File

@@ -0,0 +1,609 @@
"""自定义模板结果面板Step 6 重构)。"""
import threading
import time
from tkinter import messagebox
import tkinter as tk
import ttkbootstrap as ttk
import colour
import numpy as np
from app.data_range_converter import convert_pattern_params
def create_custom_template_result_panel(self):
"""创建客户模板结果显示区域(黑底表格)"""
self.custom_result_frame = ttk.LabelFrame(
self.custom_template_tab_frame, text="客户模板结果显示"
)
self.custom_result_frame.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
)
table_container = tk.Frame(
self.custom_result_frame,
bg="#000000",
highlightthickness=1,
highlightbackground="#5a5a5a",
)
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
style = ttk.Style()
style.configure(
"CustomResult.Treeview",
background="#000000",
fieldbackground="#000000",
foreground="#ffffff",
rowheight=28,
borderwidth=0,
)
style.configure(
"CustomResult.Treeview.Heading",
background="#2f2f2f",
foreground="#f5f5f5",
font=("Microsoft YaHei", 10, "bold"),
relief="flat",
)
style.map(
"CustomResult.Treeview",
background=[("selected", "#1f4e79")],
foreground=[("selected", "#ffffff")],
)
style.map(
"CustomResult.Treeview.Heading",
background=[("active", "#3b3b3b")],
)
columns = (
"Pattern",
"No.",
"X",
"Y",
"Z",
"x",
"y",
"Lv",
"u'",
"v'",
"Tcp",
"duv",
"λd/λc",
"Pe"
)
self.custom_result_tree = ttk.Treeview(
table_container,
columns=columns,
show="headings",
height=4,
style="CustomResult.Treeview",
)
column_widths = {
"Pattern": 90,
"No.": 60,
"X": 80,
"Y": 80,
"Z": 80,
"x": 80,
"y": 80,
"Lv": 80,
"u'": 80,
"v'": 80,
"Tcp": 90,
"duv": 80,
"λd/λc": 95,
"Pe": 80,
}
for col in columns:
self.custom_result_tree.heading(col, text=col)
self.custom_result_tree.column(
col,
width=column_widths.get(col, 80),
minwidth=60,
anchor=tk.CENTER,
stretch=False,
)
y_scroll = ttk.Scrollbar(
table_container,
orient=tk.VERTICAL,
command=self.custom_result_tree.yview,
)
x_scroll = ttk.Scrollbar(
table_container,
orient=tk.HORIZONTAL,
command=self.custom_result_tree.xview,
)
self.custom_result_tree.configure(
yscrollcommand=y_scroll.set,
xscrollcommand=x_scroll.set,
)
self.custom_result_tree.grid(row=0, column=0, sticky="nsew")
y_scroll.grid(row=0, column=1, sticky="ns")
x_scroll.grid(row=1, column=0, sticky="ew")
# 右键菜单复制全部数据Excel 可直接按行列粘贴)
self.custom_result_menu = tk.Menu(self.root, tearoff=0)
self.custom_result_menu.add_command(
label="复制全部数据",
command=self.copy_custom_result_table,
)
self.custom_result_menu.add_command(
label="单步测试",
command=self.start_custom_row_single_step,
)
# self.custom_result_menu.add_separator()
# self.custom_result_menu.add_command(
# label="单步测试",
# command=self.fill_custom_result_test_data,
# )
self.custom_result_tree.bind("<Button-3>", self.show_custom_result_context_menu)
table_container.grid_rowconfigure(0, weight=1)
table_container.grid_columnconfigure(0, weight=1)
def show_custom_result_context_menu(self, event):
"""显示客户模板结果右键菜单"""
if not hasattr(self, "custom_result_tree") or not hasattr(
self, "custom_result_menu"
):
return
if self.testing:
# 测试进行中锁定客户模板结果表,禁止右键菜单。
return
row_id = self.custom_result_tree.identify_row(event.y)
if row_id:
self.custom_result_tree.selection_set(row_id)
self.custom_result_tree.focus(row_id)
has_rows = len(self.custom_result_tree.get_children()) > 0
has_selection = len(self.custom_result_tree.selection()) > 0
can_single_step = (
has_selection
and self.ca is not None
and self.ucd is not None
and not self.testing
)
try:
self.custom_result_menu.entryconfigure(
0,
state=("normal" if has_rows else "disabled"),
)
self.custom_result_menu.entryconfigure(
1,
state=("normal" if can_single_step else "disabled"),
)
self.custom_result_menu.tk_popup(event.x_root, event.y_root)
finally:
self.custom_result_menu.grab_release()
def set_custom_result_table_locked(self, locked):
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
if not hasattr(self, "custom_result_tree"):
return
try:
self.custom_result_tree.configure(selectmode=("none" if locked else "browse"))
except Exception:
pass
def start_custom_row_single_step(self):
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
if not hasattr(self, "custom_result_tree"):
return
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
if self.testing:
messagebox.showinfo("提示", "测试进行中,无法执行单步测试")
return
selected = self.custom_result_tree.selection()
if not selected:
messagebox.showinfo("提示", "请先选中一行再执行单步测试")
return
item_id = selected[0]
values = self.custom_result_tree.item(item_id, "values")
if not values:
messagebox.showinfo("提示", "选中行没有有效数据")
return
row_no = None
if len(values) > 1:
try:
row_no = int(float(values[1]))
except Exception:
row_no = None
if row_no is None or row_no <= 0:
children = list(self.custom_result_tree.get_children())
row_no = children.index(item_id) + 1 if item_id in children else 1
self._clear_custom_result_row(item_id, row_no)
threading.Thread(
target=self._run_custom_row_single_step,
args=(item_id, row_no),
daemon=True,
).start()
def _clear_custom_result_row(self, item_id, row_no):
"""单步测试开始前清空指定行的测量数据"""
if not hasattr(self, "custom_result_tree"):
return
old_values = list(self.custom_result_tree.item(item_id, "values"))
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
cleared_values = (
pattern_name,
row_no,
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
)
self.custom_result_tree.item(item_id, values=cleared_values)
self.custom_result_tree.see(item_id)
def _run_custom_row_single_step(self, item_id, row_no):
"""后台执行客户模板单步测试"""
try:
self.root.after(0, lambda: self.status_var.set(f"单步测试第 {row_no} 行..."))
self.log_gui.log(f"开始单步测试第 {row_no}")
self.config.set_current_pattern("custom")
# 与批量 custom 测试保持一致:根据当前 SDR 配置转换 pattern 数据。
import copy
data_range = self.sdr_data_range_var.get()
original_params = copy.deepcopy(self.config.default_pattern_temp["pattern_params"])
converted_params = convert_pattern_params(
pattern_params=original_params,
data_range=data_range,
verbose=False,
)
temp_config = self.config.get_temp_config_with_converted_params(
mode="custom",
converted_params=converted_params,
)
if row_no > len(converted_params):
self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}")
self.root.after(0, lambda: self.status_var.set("单步测试失败:行号超范围"))
return
self.ucd.set_ucd_params(temp_config)
pattern_param = converted_params[row_no - 1]
self.ucd.set_pattern(self.ucd.current_pattern, pattern_param)
self.ucd.run()
time.sleep(self.pattern_settle_time)
# 测量显示模式1读取 Tcp/duv/Lv显示模式8读取 λd/Pe/Lv 与 XYZ。
self.ca.set_Display(1)
tcp, duv, lv, _, _, _ = self.ca.readAllDisplay()
self.ca.set_Display(8)
lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay()
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
row_data = {
"X": X,
"Y": Y,
"Z": Z,
"x": xy[0],
"y": xy[1],
"Lv": lv,
"u_prime": u_prime,
"v_prime": v_prime,
"Tcp": tcp,
"duv": duv,
"lambda_d": lambda_d,
"Pe": pe,
}
self.root.after(
0,
lambda: self._update_custom_result_row(item_id, row_no, row_data),
)
self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖")
self.root.after(0, lambda: self.status_var.set(f"{row_no} 行单步测试完成"))
except Exception as e:
self.log_gui.log(f"❌ 单步测试失败: {str(e)}")
self.root.after(0, lambda: self.status_var.set("单步测试失败"))
def _update_custom_result_row(self, item_id, row_no, result_data):
"""覆盖更新客户模板结果表中指定行"""
def fmt(value, digits=4):
if value is None:
return "--"
if isinstance(value, (int, float, np.floating)):
# CA 返回异常哨兵值(如 -99999999显示为占位符。
if (not np.isfinite(value)) or value <= -99999998:
return "---"
return f"{value:.{digits}f}"
try:
numeric_value = float(value)
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
return "---"
except (TypeError, ValueError):
pass
return str(value)
old_values = list(self.custom_result_tree.item(item_id, "values"))
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
new_values = (
pattern_name,
row_no,
fmt(result_data.get("X")),
fmt(result_data.get("Y")),
fmt(result_data.get("Z")),
fmt(result_data.get("x")),
fmt(result_data.get("y")),
fmt(result_data.get("Lv"), 3),
fmt(result_data.get("u_prime")),
fmt(result_data.get("v_prime")),
fmt(result_data.get("Tcp"), 1),
fmt(result_data.get("duv"), 5),
fmt(result_data.get("lambda_d"), 1),
fmt(result_data.get("Pe"), 1),
)
self.custom_result_tree.item(item_id, values=new_values)
def copy_custom_result_table(self):
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern"""
if not hasattr(self, "custom_result_tree"):
return
items = self.custom_result_tree.get_children()
if not items:
messagebox.showinfo("提示", "当前没有可复制的数据")
return
lines = []
columns = tuple(self.custom_result_tree["columns"])
excluded_col_indexes = {
idx
for idx, col_name in enumerate(columns)
if col_name in ("No.", "Pattern")
}
for item in items:
values = self.custom_result_tree.item(item, "values")
# 跳过 No. 和 Pattern 两列,只保留测量数据列。
data_values = [
v for idx, v in enumerate(values) if idx not in excluded_col_indexes
]
row = [
str(v).replace("\t", " ").replace("\n", " ")
for v in data_values
]
lines.append("\t".join(row))
clipboard_text = "\n".join(lines)
self.root.clipboard_clear()
self.root.clipboard_append(clipboard_text)
self.root.update_idletasks()
if hasattr(self, "status_var"):
self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ 已复制客户模板表格数据({len(items)} 行)")
def fill_custom_result_test_data(self):
"""填充 147 行客户模板测试数据(用于界面验证)"""
if not hasattr(self, "custom_result_tree"):
return
self.clear_custom_template_results()
pattern_names = []
if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
pattern_names = self.config.get_temp_pattern_names()
total_rows = 147
for i in range(1, total_rows + 1):
ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
row_data = {
"pattern_name": (
pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
),
"X": 0.8 + ratio * 120,
"Y": 0.9 + ratio * 135,
"Z": 1.1 + ratio * 145,
"x": 0.24 + ratio * 0.10,
"y": 0.26 + ratio * 0.10,
"Lv": 1.0 + ratio * 500,
"u_prime": 0.16 + ratio * 0.12,
"v_prime": 0.42 + ratio * 0.08,
"Tcp": 1800 + ratio * 12000,
"duv": -0.01 + ratio * 0.03,
"lambda_d": 430 + ratio * 200,
"Pe": 10 + ratio * 90,
}
self.append_custom_template_result(i, row_data)
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
self.chart_notebook.select(self.custom_template_tab_frame)
if hasattr(self, "status_var"):
self.status_var.set("已填充 147 行客户模板测试数据")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已填充 147 行客户模板测试数据")
def clear_custom_template_results(self):
"""清空客户模板结果表格"""
if not hasattr(self, "custom_result_tree"):
return
for item in self.custom_result_tree.get_children():
self.custom_result_tree.delete(item)
def auto_expand_custom_result_view(self):
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
if not hasattr(self, "custom_result_tree"):
return
if len(self.custom_result_tree.get_children()) == 0:
return
try:
self.root.update_idletasks()
columns = tuple(self.custom_result_tree["columns"])
columns_total_width = 0
for col in columns:
columns_total_width += int(self.custom_result_tree.column(col, "width"))
left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180
if left_panel_width <= 1:
left_panel_width = 180
# 列宽 + 左侧导航 + 滚动条/边框/外边距。
target_width = int(left_panel_width + columns_total_width + 120)
screen_max_width = max(900, self.root.winfo_screenwidth() - 40)
target_width = min(target_width, screen_max_width)
current_width = self.root.winfo_width()
current_height = self.root.winfo_height()
# 只扩不缩,避免用户窗口被反复改变。
if target_width > current_width:
self.root.geometry(f"{target_width}x{current_height}")
self.root.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ 自动扩展客户模板窗口失败: {str(e)}")
def append_custom_template_result(self, row_no, result_data):
"""追加一条客户模板结果到表格"""
def fmt(value, digits=4):
if value is None:
return "--"
if isinstance(value, (int, float, np.floating)):
# CA 返回异常哨兵值(如 -99999999显示为占位符。
if (not np.isfinite(value)) or value <= -99999998:
return "---"
return f"{value:.{digits}f}"
try:
numeric_value = float(value)
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
return "---"
except (TypeError, ValueError):
pass
return str(value)
row_values = (
result_data.get("pattern_name", f"P {row_no}"),
row_no,
fmt(result_data.get("X")),
fmt(result_data.get("Y")),
fmt(result_data.get("Z")),
fmt(result_data.get("x")),
fmt(result_data.get("y")),
fmt(result_data.get("Lv"), 3),
fmt(result_data.get("u_prime")),
fmt(result_data.get("v_prime")),
fmt(result_data.get("Tcp"), 1),
fmt(result_data.get("duv"), 5),
fmt(result_data.get("lambda_d"), 1),
fmt(result_data.get("Pe"), 1)
)
if hasattr(self, "custom_result_tree"):
item_id = self.custom_result_tree.insert("", tk.END, values=row_values)
# 新增数据后自动跳转到最新行。
self.custom_result_tree.see(item_id)
self.auto_expand_custom_result_view()
def start_custom_template_test(self):
"""开始客户模板测试SDR"""
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
self.chart_notebook.select(self.custom_template_tab_frame)
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
if self.testing:
messagebox.showinfo("提示", "测试已在进行中")
return
if hasattr(self, "debug_container"):
self.debug_container.pack_forget()
self.testing = True
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.save_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.DISABLED)
self.custom_btn.config(state=tk.DISABLED)
self.status_var.set("客户模板测试进行中...")
self.log_gui.clear_log()
self.clear_custom_template_results()
confirm = messagebox.askyesno(
"确认测试", "开始客户模板测试SDR\n\n将采集并显示客户模板格式结果。"
)
if not confirm:
self.testing = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.NORMAL)
self.custom_btn.config(state=tk.NORMAL)
self.status_var.set("测试已取消")
self.set_custom_result_table_locked(False)
return
self.set_custom_result_table_locked(True)
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
self.test_thread.daemon = True
self.test_thread.start()

View File

@@ -0,0 +1,498 @@
"""主布局面板创建函数Step 6 重构)。"""
import tkinter as tk
import ttkbootstrap as ttk
from drivers.UCD323_Enum import UCDEnum
from app.views.collapsing_frame import CollapsingFrame
from app.resources import load_icon
def create_floating_config_panel(self):
"""创建右上角悬浮配置框"""
cf = CollapsingFrame(self.control_frame_top)
cf.pack(fill="both")
# 创建悬浮框主容器
self.config_panel_frame = ttk.Frame(cf)
cf.add(self.config_panel_frame, title="配置项")
# 创建一个统一的frame来替代选项卡控件
self.config_content_frame = ttk.Frame(self.config_panel_frame)
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建一个横向排列的Frame
config_row_frame = ttk.Frame(self.config_content_frame)
config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5)
# 创建连接内容区域
self.connection_frame = ttk.LabelFrame(config_row_frame, text="设备连接")
self.connection_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建测试项目区域
self.test_items_frame = ttk.LabelFrame(config_row_frame, text="测试项目")
self.test_items_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建信号格式区域
self.signal_format_frame = ttk.LabelFrame(config_row_frame, text="信号格式")
self.signal_format_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建连接内容
self.create_connection_content()
# 创建测试项目内容
self.create_test_items_content()
# 创建信号格式内容
self.create_signal_format_content()
self.config_panel_frame.grid_remove()
self.config_panel_frame.btn.configure(image="closed")
def create_test_items_content(self):
"""创建测试项目选项卡内容"""
# 创建测试项目字典,用于管理不同测试类型的选项
self.test_items = {
"screen_module": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
],
},
"sdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
"hdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("EOTF", "eotf"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
}
# 根据当前测试类型创建复选框
self.test_vars = {}
self.update_test_items()
# 创建色度参数设置框架
self.create_cct_params_frame()
def create_signal_format_content(self):
"""创建信号格式选项卡内容"""
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
# ==================== 屏模组格式设置 ====================
self.screen_module_signal_frame = ttk.Frame(self.signal_tabs)
self.screen_module_signal_frame.grid_columnconfigure(0, weight=1)
self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试")
self.screen_module_timing_var = tk.StringVar(
value=self.config.current_test_types[self.config.current_test_type][
"timing"
]
)
screen_module_timing_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
state="readonly",
)
screen_module_timing_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_timing_changed
)
screen_module_timing_combo.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
# ==================== SDR信号格式设置 ====================
self.sdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.sdr_signal_frame.grid_columnconfigure(0, weight=0)
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
# 色彩空间
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_color_space_var = tk.StringVar(value="BT.709")
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["BT.709", "BT.601", "BT.2020"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_gamma_type_var = tk.StringVar(value="2.2")
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=["2.2", "2.4", "2.6"],
width=10,
state="readonly",
)
sdr_gamma_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# 数据范围
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_data_range_var = tk.StringVar(value="Full")
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=["Full", "Limited"],
width=10,
state="readonly",
)
sdr_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_bit_depth_var = tk.StringVar(value="8bit")
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== HDR信号格式设置 ====================
self.hdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.hdr_signal_frame.grid_columnconfigure(0, weight=0)
self.hdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.hdr_signal_frame, text="HDR")
# 色彩空间
ttk.Label(self.hdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_color_space_var = tk.StringVar(value="BT.2020")
hdr_color_space_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_color_space_var,
values=["BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
hdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Metadata设置
ttk.Label(self.hdr_signal_frame, text="Metadata:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_metadata_frame = ttk.Frame(self.hdr_signal_frame)
self.hdr_metadata_frame.grid(
row=1, column=1, rowspan=2, sticky=tk.W, padx=5, pady=2
)
ttk.Label(self.hdr_metadata_frame, text="MaxCLL:").grid(
row=0, column=0, sticky=tk.W
)
self.hdr_maxcll_var = tk.StringVar(value="1000")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxcll_var, width=6
).grid(row=0, column=1, padx=2)
ttk.Label(self.hdr_metadata_frame, text="MaxFALL:").grid(
row=1, column=0, sticky=tk.W
)
self.hdr_maxfall_var = tk.StringVar(value="400")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxfall_var, width=6
).grid(row=1, column=1, padx=2)
# 数据范围
ttk.Label(self.hdr_signal_frame, text="数据范围:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_data_range_var = tk.StringVar(value="Full")
hdr_range_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_data_range_var,
values=["Full", "Limited"],
width=10,
state="readonly",
)
hdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.hdr_signal_frame, text="编码位深:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_bit_depth_var = tk.StringVar(value="8bit")
hdr_bit_depth_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
width=10,
state="readonly",
)
hdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== 初始化:默认只启用屏模组 Tab ====================
self.signal_tabs.select(0) # 选中屏模组
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
def create_connection_content(self):
"""创建设备连接区域"""
# 创建设备连接区域的主框架
com_frame = ttk.Frame(self.connection_frame)
com_frame.pack(fill=tk.X, pady=5)
# 获取可用的COM端口列表
available_ports = self.get_available_com_ports()
# 使用网格布局,更整齐
ttk.Label(com_frame, text="UCD列表:").grid(
row=0, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ucd_list_var = tk.StringVar(value=self.config.device_config["ucd_list"])
self.ucd_list_combo = ttk.Combobox(
com_frame,
textvariable=self.ucd_list_var,
values=available_ports,
width=10,
state="readonly",
)
self.ucd_list_combo.grid(row=0, column=1, sticky=ttk.W, padx=5, pady=3)
self.ucd_list_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加UCD连接状态指示器
self.ucd_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
self.ucd_status_indicator.config(bg="gray")
# 添加按钮框架
button_frame = ttk.Frame(com_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="w")
connect_icon = load_icon("assets/connect-svgrepo-com.png")
self.check_button = ttk.Button(
button_frame,
image=connect_icon,
bootstyle="link",
takefocus=False,
command=self.check_com_connections,
)
self.check_button.image = connect_icon
self.check_button.pack(side="left", padx=0, pady=3)
disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
# 断开连接按钮
self.disconnect_button = ttk.Button(
button_frame,
image=disconnect_icon,
bootstyle="link",
takefocus=False,
command=self.disconnect_com_connections,
)
self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
self.disconnect_button.pack(side="left", padx=0, pady=3)
refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
self.refresh_button = ttk.Button(
button_frame,
image=refresh_icon,
bootstyle="link",
takefocus=False,
command=self.refresh_com_ports,
)
self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
self.refresh_button.pack(side="left", padx=0, pady=3)
# CA端口
ttk.Label(com_frame, text="CA端口:").grid(
row=1, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ca_com_var = tk.StringVar(value=self.config.device_config["ca_com"])
self.ca_com_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_com_var,
values=available_ports,
width=10,
state="readonly",
)
self.ca_com_combo.grid(row=1, column=1, sticky=ttk.W, padx=5, pady=3)
self.ca_com_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加CA连接状态指示器
self.ca_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
self.ca_status_indicator.config(bg="gray")
# 添加CA通道设置
ttk.Label(com_frame, text="CA通道:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=3
)
self.ca_channel_var = tk.StringVar(
value=self.config.device_config["ca_channel"]
)
ca_channel_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_channel_var,
values=[str(i) for i in range(11)],
width=10,
state="readonly",
)
ca_channel_combo.grid(row=2, column=1, sticky=ttk.W, padx=5, pady=3)
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
def create_test_type_frame(self):
"""创建测试类型选择区域(侧边栏形式)"""
# 设置测试类型变量
self.test_type_var = tk.StringVar(value="screen_module")
# 创建测试类型按钮并放置在侧边栏
test_types = [
("屏模组性能测试", "screen_module"),
("SDR Movie测试", "sdr_movie"),
("HDR Movie测试", "hdr_movie"),
]
for text, type_value in test_types:
btn = ttk.Button(
master=self.sidebar_frame,
text=text,
style="Sidebar.TButton",
padding=10,
command=lambda v=type_value: self.change_test_type(v),
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
# 保存按钮引用以便后续更新样式
setattr(self, f"{type_value}_btn", btn)
# 添加分隔线
ttk.Separator(self.sidebar_frame, orient="horizontal").pack(
fill=tk.X, padx=10, pady=10
)
# ✅ 只保留日志按钮
self.log_btn = ttk.Button(
self.sidebar_frame,
text="测试日志",
style="Sidebar.TButton",
command=self.toggle_log_panel,
takefocus=False,
)
self.log_btn.pack(fill=tk.X, padx=0, pady=1)
# Local Dimming 测试按钮
self.local_dimming_btn = ttk.Button(
self.sidebar_frame,
text="Local Dimming",
style="Sidebar.TButton",
command=self.toggle_local_dimming_panel,
takefocus=False,
)
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
# 注册面板按钮(只保留日志)
if hasattr(self, "panels"):
if "log" in self.panels:
self.panels["log"]["button"] = self.log_btn
if "local_dimming" in self.panels:
self.panels["local_dimming"]["button"] = self.local_dimming_btn
def update_config_info_display(self):
"""更新配置信息显示"""
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
current_config = self.config.get_current_config()
info_text = f"测试类型: {current_config.get('name', '未知')}\n"
info_text += (
f"测试项目: {', '.join(current_config.get('test_items', []))}\n"
)
info_text += f"信号格式: {current_config.get('signal_format', 'none')}\n"
info_text += f"色彩空间: {current_config.get('color_space', 'unknown')}\n"
info_text += f"位深度: {current_config.get('bit_depth', 'unknown')}"
# 高亮当前选中的测试类型
self.update_sidebar_selection()
def create_operation_frame(self):
"""创建操作按钮区域"""
operation_frame = ttk.Frame(self.control_frame_top)
operation_frame.pack(fill=tk.X, padx=5, pady=10)
self.start_btn = ttk.Button(
operation_frame,
text="开始测试",
command=self.start_test,
style="success.TButton",
)
self.start_btn.pack(side=tk.LEFT, padx=5)
self.stop_btn = ttk.Button(
operation_frame,
text="停止测试",
command=self.stop_test,
style="danger.TButton",
state=tk.DISABLED,
)
self.stop_btn.pack(side=tk.LEFT, padx=5)
self.save_btn = ttk.Button(
operation_frame,
text="保存结果",
command=self.save_results,
state=tk.DISABLED,
)
self.save_btn.pack(side=tk.LEFT, padx=5)
self.clear_config_btn = ttk.Button(
operation_frame,
text="清理配置",
command=self.clear_config_file,
)
self.clear_config_btn.pack(side=tk.LEFT, padx=5)
self.custom_btn = ttk.Button(
operation_frame,
text="客户模版",
command=self.start_custom_template_test,
style="info.TButton",
)
self.custom_btn.pack(side=tk.LEFT, padx=5)
self.update_custom_button_visibility()

View File

@@ -0,0 +1,412 @@
"""侧边面板(日志 / Local Dimming / 调试Step 6 重构)。"""
import traceback
import tkinter as tk
import ttkbootstrap as ttk
from app.views.pq_log_gui import PQLogGUI
from app.views.pq_debug_panel import PQDebugPanel
def create_log_panel(self):
"""创建日志面板"""
self.log_frame = ttk.Frame(self.content_frame)
self.log_gui = PQLogGUI(self.log_frame)
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 默认隐藏日志面板
self.log_visible = False
# 注册到面板管理系统
self.register_panel(
"log", self.log_frame, None, "log_visible"
) # button会在后面设置
def create_local_dimming_panel(self):
"""创建 Local Dimming 测试面板 - 手动控制版"""
self.local_dimming_frame = ttk.Frame(self.content_frame)
# 主容器
main_container = ttk.Frame(self.local_dimming_frame, padding=10)
main_container.pack(fill=tk.BOTH, expand=True)
# ==================== 1. 标题 ====================
title_frame = ttk.Frame(main_container)
title_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(
title_frame,
text="🔆 Local Dimming 窗口测试",
font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT)
# ==================== 2. 窗口百分比按钮 ====================
window_frame = ttk.LabelFrame(
main_container, text="🔆 窗口百分比(点击发送)", padding=10
)
window_frame.pack(fill=tk.X, pady=(0, 10))
# 说明文字
ttk.Label(
window_frame,
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
font=("", 9),
foreground="#28a745",
).pack(pady=(0, 8))
# 第一行1%, 2%, 5%, 10%, 18%
row1 = ttk.Frame(window_frame)
row1.pack(fill=tk.X, pady=(0, 5))
percentages_row1 = [1, 2, 5, 10, 18]
for p in percentages_row1:
ttk.Button(
row1,
text=f"{p}%",
command=lambda p=p: self.send_ld_window(p),
bootstyle="success",
width=12,
).pack(side=tk.LEFT, padx=3)
# 第二行25%, 50%, 75%, 100%
row2 = ttk.Frame(window_frame)
row2.pack(fill=tk.X)
percentages_row2 = [25, 50, 75, 100]
for p in percentages_row2:
ttk.Button(
row2,
text=f"{p}%",
command=lambda p=p: self.send_ld_window(p),
bootstyle="success",
width=12,
).pack(side=tk.LEFT, padx=3)
# ==================== 4. CA410 采集按钮 ====================
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10)
measure_frame.pack(fill=tk.X, pady=(0, 10))
measure_btn_frame = ttk.Frame(measure_frame)
measure_btn_frame.pack(fill=tk.X)
self.ld_measure_btn = ttk.Button(
measure_btn_frame,
text="📏 采集当前亮度",
command=self.measure_ld_luminance,
bootstyle="primary",
width=15,
)
self.ld_measure_btn.pack(side=tk.LEFT, padx=(0, 5))
# 显示测量结果
self.ld_result_label = ttk.Label(
measure_btn_frame,
text="亮度: -- cd/m² | x: -- | y: --",
font=("Consolas", 10),
foreground="#007bff",
)
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
# ==================== 5. 测试结果表格 ====================
result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Treeview
columns = ("窗口百分比", "亮度 (cd/m²)", "x", "y", "时间")
self.ld_tree = ttk.Treeview(
result_frame, columns=columns, show="headings", height=10
)
for col in columns:
self.ld_tree.heading(col, text=col)
if col == "窗口百分比":
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
elif col == "时间":
self.ld_tree.column(col, width=120, anchor=tk.CENTER)
else:
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
self.ld_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 滚动条
scrollbar = ttk.Scrollbar(
result_frame, orient=tk.VERTICAL, command=self.ld_tree.yview
)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.ld_tree.configure(yscrollcommand=scrollbar.set)
# ==================== 6. 底部操作按钮 ====================
bottom_frame = ttk.Frame(main_container)
bottom_frame.pack(fill=tk.X)
self.ld_clear_btn = ttk.Button(
bottom_frame,
text="🗑️ 清空记录",
command=self.clear_ld_records,
bootstyle="danger-outline",
width=12,
)
self.ld_clear_btn.pack(side=tk.LEFT, padx=(0, 5))
self.ld_save_btn = ttk.Button(
bottom_frame,
text="💾 保存结果",
command=self.save_local_dimming_results,
bootstyle="info",
width=12,
)
self.ld_save_btn.pack(side=tk.LEFT)
# 默认隐藏
self.local_dimming_visible = False
# 注册到面板管理系统
self.register_panel(
"local_dimming",
self.local_dimming_frame,
None,
"local_dimming_visible",
)
# 初始化当前窗口百分比(用于记录)
self.current_ld_percentage = None
def toggle_local_dimming_panel(self):
"""切换 Local Dimming 面板显示"""
self.show_panel("local_dimming")
def toggle_log_panel(self):
"""切换日志面板的显示状态"""
self.show_panel("log")
def toggle_screen_debug_panel(self):
"""打开/关闭屏模组单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "debug_window") and self.debug_window.winfo_exists():
self.debug_window.destroy()
self.screen_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ 单步调试面板已关闭")
return
# 创建新窗口
self.debug_window = ttk.Toplevel(self.root)
self.debug_window.title("🔧 单步调试面板")
self.debug_window.geometry("900x400")
self.debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True) # ← 这个 pack 是对的
# 创建调试面板实例
from app.views.pq_debug_panel import PQDebugPanel
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 这里不应该有任何 pack 调用!
self.log_gui.log("✓ 单步调试面板实例已创建")
# 重新启用调试(如果有数据)
try:
test_type = self.config.current_test_type
selected_items = self.get_selected_test_items()
if test_type == "screen_module":
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug(
"screen_module", "gamma", gray_data
)
self.log_gui.log("✓ 屏模组 Gamma 单步调试已重新启用")
else:
self.log_gui.log(" ✗ 没有可用的灰阶数据")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug(
"screen_module", "rgb", rgb_data
)
self.log_gui.log("✓ 屏模组 RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.screen_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.screen_debug_btn.config(text="打开调试面板")
self.debug_window.destroy()
self.log_gui.log("✓ 单步调试窗口已关闭")
self.debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.debug_window.update_idletasks()
self.log_gui.log("✓ 单步调试面板已打开(独立窗口)")
def toggle_sdr_debug_panel(self):
"""打开/关闭 SDR 单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "sdr_debug_window") and self.sdr_debug_window.winfo_exists():
self.sdr_debug_window.destroy()
self.sdr_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ SDR 单步调试面板已关闭")
return
# 创建新窗口
self.sdr_debug_window = ttk.Toplevel(self.root)
self.sdr_debug_window.title("🔧 SDR 单步调试面板")
self.sdr_debug_window.geometry("900x400")
self.sdr_debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.sdr_debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True)
# ✅ 创建调试面板实例(不要对它调用 pack
from app.views.pq_debug_panel import PQDebugPanel
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 删除debug_panel_instance.pack(...)
self.log_gui.log("✓ SDR 单步调试面板实例已创建")
# ✅ 重新启用调试(如果有数据)
try:
selected_items = self.get_selected_test_items()
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug("sdr_movie", "gamma", gray_data)
self.log_gui.log("✓ SDR Gamma 单步调试已重新启用")
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
debug_panel_instance.enable_debug(
"sdr_movie", "accuracy", accuracy_data
)
self.log_gui.log("✓ SDR 色准单步调试已重新启用")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug("sdr_movie", "rgb", rgb_data)
self.log_gui.log("✓ SDR RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载 SDR 调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.sdr_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.sdr_debug_btn.config(text="打开调试面板")
self.sdr_debug_window.destroy()
self.log_gui.log("✓ SDR 单步调试窗口已关闭")
self.sdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.sdr_debug_window.update_idletasks()
self.log_gui.log("✓ SDR 单步调试面板已打开(独立窗口)")
def toggle_hdr_debug_panel(self):
"""打开/关闭 HDR 单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "hdr_debug_window") and self.hdr_debug_window.winfo_exists():
self.hdr_debug_window.destroy()
self.hdr_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ HDR 单步调试面板已关闭")
return
# 创建新窗口
self.hdr_debug_window = ttk.Toplevel(self.root)
self.hdr_debug_window.title("🔧 HDR 单步调试面板")
self.hdr_debug_window.geometry("900x400")
self.hdr_debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.hdr_debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True)
# ✅ 创建调试面板实例(不要对它调用 pack
from app.views.pq_debug_panel import PQDebugPanel
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 删除debug_panel_instance.pack(...)
self.log_gui.log("✓ HDR 单步调试面板实例已创建")
# ✅ 重新启用调试(如果有数据)
try:
selected_items = self.get_selected_test_items()
if "eotf" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug("hdr_movie", "eotf", gray_data)
self.log_gui.log("✓ HDR EOTF 单步调试已重新启用")
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
debug_panel_instance.enable_debug(
"hdr_movie", "accuracy", accuracy_data
)
self.log_gui.log("✓ HDR 色准单步调试已重新启用")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug("hdr_movie", "rgb", rgb_data)
self.log_gui.log("✓ HDR RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载 HDR 调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.hdr_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.hdr_debug_btn.config(text="打开调试面板")
self.hdr_debug_window.destroy()
self.log_gui.log("✓ HDR 单步调试窗口已关闭")
self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.hdr_debug_window.update_idletasks()
self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)")

1230
app/views/pq_debug_panel.py Normal file

File diff suppressed because it is too large Load Diff

31
app/views/pq_log_gui.py Normal file
View File

@@ -0,0 +1,31 @@
import tkinter as tk
import ttkbootstrap as ttk
class PQLogGUI(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.create_widgets()
def create_widgets(self):
log_frame = ttk.LabelFrame(self, text="测试日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.log_text = ttk.Text(log_frame, height=8, width=50)
self.log_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
log_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
self.log_text.config(yscrollcommand=log_scrollbar.set)
self.log_text.config(state=tk.DISABLED)
def log(self, message):
self.log_text.config(state=tk.NORMAL)
self.log_text.insert(tk.END, message + "\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def clear_log(self):
self.log_text.config(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.config(state=tk.DISABLED)