Files
pqAutomationApp/utils/local_dimming_test.py
xinzhu.yin c157e774e5 1.1.0版本
2026-04-16 16:51:05 +08:00

586 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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}")