1.1.0版本

This commit is contained in:
xinzhu.yin
2026-04-16 16:51:05 +08:00
commit c157e774e5
333 changed files with 70759 additions and 0 deletions

585
utils/local_dimming_test.py Normal file
View File

@@ -0,0 +1,585 @@
"""
Local Dimming 测试模块
功能:
- 生成不同百分比的白色窗口图片
- 通过 UCD 发送图片到显示器
- 自动采集 CA410 亮度数据
- 记录并导出测试结果
"""
import os
import sys
import time
import atexit
import shutil
import numpy as np
from PIL import Image, ImageDraw
import UniTAP
class LocalDimmingController:
"""Local Dimming 控制器 - 用于发送不同百分比窗口 Pattern"""
def __init__(self, ucd_controller):
"""
初始化 Local Dimming 控制器
Args:
ucd_controller: UCD323 控制器实例
"""
self.ucd = ucd_controller
# 兼容打包后的路径
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
self.temp_dir = os.path.join(base_dir, "temp_local_dimming")
# 创建临时目录
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
print(f"[LD] 创建临时目录: {self.temp_dir}")
self.cached_images = {} # 缓存已生成的图片 {(分辨率, 百分比): 文件路径}
# 注册退出时自动清理
atexit.register(self.cleanup)
print("[LD] Local Dimming 控制器已初始化")
def send_window_pattern_with_resolution(self, percentage, width, height):
"""
发送指定百分比和分辨率的白色窗口 Pattern
Args:
percentage: 窗口面积百分比 (1-100)
width: 图像宽度
height: 图像高度
Returns:
bool: 是否成功
"""
try:
# 检查设备连接状态
if not self.ucd.status:
print("[LD 错误] 设备未连接")
return False
print(f"\n[LD] 开始发送 {percentage}% 窗口 Pattern")
print(f"[LD] 使用分辨率: {width}x{height}")
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
# UCD323Controller多接口支持
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
print(f"[LD 错误] 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 先停止音频,避免蜂鸣声
try:
ag.stop_generate()
print("[LD] 已停止音频生成")
except Exception as e:
print(f"[LD 警告] 停止音频失败: {e}")
# 检查缓存
cache_key = (f"{width}x{height}", percentage)
if cache_key in self.cached_images:
image_path = self.cached_images[cache_key]
if os.path.exists(image_path):
print(f"[LD] 使用缓存图片: {image_path}")
else:
print(f"[LD] 缓存图片不存在,重新生成...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
else:
print(f"[LD] 正在生成 {percentage}% 窗口图像...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
# 发送图像到设备
print(f"[LD] 正在发送图像到设备...")
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 如果有 timing设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
print(f"[LD] {percentage}% 窗口 Pattern 已发送到设备")
return True
except Exception as e:
print(f"[LD 异常] 发送 {percentage}% 窗口失败: {e}")
import traceback
traceback.print_exc()
return False
def send_window_pattern(self, percentage):
"""
发送指定百分比的白色窗口 Pattern从 GUI 获取分辨率)
Args:
percentage: 窗口面积百分比 (1-100)
Returns:
bool: 是否成功
"""
# 从设备当前 timing 获取分辨率
width, height = self.get_current_resolution()
return self.send_window_pattern_with_resolution(percentage, width, height)
def get_current_resolution(self):
"""
从设备当前 timing 获取显示器分辨率
Returns:
tuple: (width, height)
"""
try:
# 方式1从 Pattern Generator 的当前 VideoMode 获取
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
else:
pg = None
else:
pg = self.ucd.role.hdtx.pg
if pg:
current_vm = pg.get_vm()
if current_vm and hasattr(current_vm, "timing") and current_vm.timing:
timing = current_vm.timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从当前 timing 获取分辨率: {width}x{height}")
return width, height
# 方式2从 current_timing 属性获取
if hasattr(self.ucd, "current_timing") and self.ucd.current_timing:
timing = self.ucd.current_timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从 current_timing 获取分辨率: {width}x{height}")
return width, height
except Exception as e:
print(f"[LD 警告] 获取分辨率失败: {e}")
print("[LD 警告] 使用默认分辨率 3840x2160")
return 3840, 2160
def _generate_and_save_image(self, width, height, percentage, cache_key):
"""
生成并保存窗口图像
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比
cache_key: 缓存键
Returns:
str: 图像文件路径
"""
# 生成图像
image_array = self._create_window_image(width, height, percentage)
# 保存到项目目录
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
image = Image.fromarray(image_array, mode="RGB")
image.save(image_path, format="PNG")
# 缓存
self.cached_images[cache_key] = image_path
print(f"[LD] 图像已保存: {image_path}")
return image_path
def _create_window_image(self, width, height, percentage):
"""
创建窗口图像
黑色背景 + 居中白色矩形窗口(保持屏幕比例)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
numpy.ndarray: RGB 图像数组 (height, width, 3)
"""
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
print(
f"[LD] 图像生成完成: {width}x{height}, 窗口 {window_width}x{window_height}"
)
return image
def cleanup(self):
"""清理临时文件夹"""
if os.path.exists(self.temp_dir):
try:
shutil.rmtree(self.temp_dir)
print(f"[LD] 临时文件夹已删除: {self.temp_dir}")
except Exception as e:
print(f"[LD 警告] 删除临时文件夹失败: {e}")
def __del__(self):
"""析构函数:清理临时文件(备用机制)"""
try:
self.cleanup()
except:
pass
class LocalDimmingTest:
def __init__(self, ucd_controller, ca_serial, log_callback=None):
"""
初始化 Local Dimming 测试
Args:
ucd_controller: UCD323 控制器实例
ca_serial: CA410 串口实例
log_callback: 日志回调函数
"""
self.ucd = ucd_controller
self.ca = ca_serial
self.log = log_callback if log_callback else print
# 临时图片目录
self.temp_dir = self._init_temp_dir()
# 测试结果
self.test_results = []
# 测试配置
self.window_percentages = [1, 2, 5, 10, 18, 25, 50, 75, 100]
self.wait_time = 2.0 # 每次切换后等待时间(秒)
# 停止标志
self.stop_flag = False
self.log("✓ Local Dimming 测试模块已初始化")
def _init_temp_dir(self):
"""初始化临时目录"""
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
temp_dir = os.path.join(base_dir, "temp_local_dimming")
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def generate_window_image(self, width, height, percentage):
"""
生成窗口图片(黑色背景 + 居中白色矩形窗口)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
str: 图片文件路径
"""
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
# 保存图片
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
pil_image = Image.fromarray(image, mode="RGB")
pil_image.save(image_path, format="PNG")
self.log(f" ✓ 图片已生成: {window_width}×{window_height} px")
return image_path
def send_image_to_ucd(self, image_path):
"""
通过 UCD 发送图片到显示器
Args:
image_path: 图片文件路径
Returns:
bool: 是否成功
"""
try:
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
self.log(f" ❌ 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 停止音频
try:
ag.stop_generate()
except:
pass
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
return True
except Exception as e:
self.log(f" ❌ 发送图片失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def measure_luminance(self):
"""
使用 CA410 采集亮度
Returns:
tuple: (x, y, lv, X, Y, Z) 或 None
"""
try:
if not self.ca:
self.log(" ❌ CA410 未连接")
return None
# 采集数据
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
if x is not None and y is not None and lv is not None:
self.log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
return (x, y, lv, X, Y, Z)
else:
self.log(" ❌ 采集数据失败")
return None
except Exception as e:
self.log(f" ❌ 采集亮度异常: {str(e)}")
return None
def run_test(self, resolution="3840x2160"):
"""
执行完整的 Local Dimming 测试
Args:
resolution: 分辨率字符串,如 "3840x2160"
Returns:
list: 测试结果 [(百分比, x, y, lv, X, Y, Z), ...]
"""
self.log("=" * 60)
self.log("开始 Local Dimming 测试")
self.log("=" * 60)
# 重置停止标志
self.stop_flag = False
# 解析分辨率
try:
width, height = map(int, resolution.split("x"))
except:
width, height = 3840, 2160
self.log(f" ⚠️ 分辨率解析失败,使用默认值: {width}x{height}")
self.log(f" 分辨率: {width}x{height}")
self.log(f" 测试窗口: {self.window_percentages}")
self.log(f" 等待时间: {self.wait_time}")
self.log("")
self.test_results = []
for i, percentage in enumerate(self.window_percentages, start=1):
# 检查停止标志
if self.stop_flag:
self.log("⚠️ 测试已停止")
break
self.log(f"[{i}/{len(self.window_percentages)}] 测试 {percentage}% 窗口...")
# 1. 生成图片
image_path = self.generate_window_image(width, height, percentage)
# 2. 发送到 UCD
if not self.send_image_to_ucd(image_path):
self.log(f"{percentage}% 窗口发送失败,跳过")
continue
# 3. 等待稳定
self.log(f" ⏳ 等待 {self.wait_time} 秒...")
time.sleep(self.wait_time)
# 4. 采集亮度
result = self.measure_luminance()
if result:
x, y, lv, X, Y, Z = result
self.test_results.append((percentage, x, y, lv, X, Y, Z))
self.log(f"{percentage}% 窗口测试完成")
else:
self.log(f"{percentage}% 窗口采集失败")
self.log("")
self.log("=" * 60)
self.log("✅ Local Dimming 测试完成")
self.log(
f" 成功测试: {len(self.test_results)}/{len(self.window_percentages)} 个窗口"
)
self.log("=" * 60)
return self.test_results
def stop(self):
"""停止测试"""
self.stop_flag = True
self.log("⚠️ 正在停止测试...")
def get_results_summary(self):
"""获取测试结果摘要"""
if not self.test_results:
return None
luminances = [lv for _, _, _, lv, _, _, _ in self.test_results]
return {
"data_points": self.test_results,
"max_luminance": max(luminances),
"min_luminance": min(luminances),
"avg_luminance": sum(luminances) / len(luminances),
}
def cleanup(self):
"""清理临时文件"""
try:
import shutil
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
self.log(f"✓ 临时文件已清理: {self.temp_dir}")
except Exception as e:
self.log(f"⚠️ 清理临时文件失败: {e}")