修改摸底测试功能、修改pattern控制逻辑

This commit is contained in:
xinzhu.yin
2026-05-13 17:17:13 +08:00
parent 9a52e34f2b
commit 0513f810bd
13 changed files with 479 additions and 3067 deletions

View File

@@ -344,6 +344,8 @@ class PQConfig:
temp_config.current_pattern = copy.deepcopy(self.default_pattern_gray) temp_config.current_pattern = copy.deepcopy(self.default_pattern_gray)
elif mode == "accuracy": elif mode == "accuracy":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_accuracy) temp_config.current_pattern = copy.deepcopy(self.default_pattern_accuracy)
elif mode == "custom":
temp_config.current_pattern = copy.deepcopy(self.default_pattern_temp)
# 3. 替换为转换后的参数 # 3. 替换为转换后的参数
temp_config.current_pattern["pattern_params"] = converted_params temp_config.current_pattern["pattern_params"] = converted_params

View File

@@ -13,7 +13,6 @@ import colour
import numpy as np import numpy as np
import algorithm.pq_algorithm as pq_algorithm import algorithm.pq_algorithm as pq_algorithm
from app.data_range_converter import convert_pattern_params
from app.pq.pq_result import PQResult from app.pq.pq_result import PQResult
def new_pq_results(self, test_type, test_name): def new_pq_results(self, test_type, test_name):
@@ -310,205 +309,12 @@ def send_fix_pattern(self, mode):
results = [] results = []
try: try:
# 1. 设置图案模式 session = self.pattern_service.prepare_session(mode, log_details=True)
if mode == "rgb":
self.config.set_current_pattern("rgb")
elif mode == "gray":
self.config.set_current_pattern("gray")
elif mode == "accuracy": # 色准模式SDR 和 HDR 通用 29色
self.config.set_current_pattern("accuracy")
elif mode == "custom":
self.config.set_current_pattern("custom")
else:
self.log_gui.log(f"未知的图案模式: {mode}", level="error")
return None
# 2. 获取当前测试类型
test_type = self.config.current_test_type
# 3. 根据测试类型设置信号格式和图案
if test_type == "screen_module":
# 屏模组测试:使用 Timing
self.log_gui.log("=" * 50, level="separator")
self.log_gui.log("设置屏模组信号格式:", level="info")
self.log_gui.log("=" * 50, level="separator")
timing_str = self.config.current_test_types[test_type]["timing"]
self.log_gui.log(f" Timing: {timing_str}", level="info")
# 屏模组测试:直接使用原始配置
self.ucd.set_ucd_params(self.config)
elif test_type == "sdr_movie":
# SDR 测试设置色彩空间、Gamma 等
self.log_gui.log("=" * 50, level="separator")
self.log_gui.log("设置 SDR 信号格式:", level="info")
self.log_gui.log("=" * 50, level="separator")
color_space = self.sdr_color_space_var.get()
gamma = self.sdr_gamma_type_var.get()
data_range = self.sdr_data_range_var.get()
bit_depth = self.sdr_bit_depth_var.get()
self.log_gui.log(f" 色彩空间: {color_space}", level="info")
self.log_gui.log(f" Gamma: {gamma}", level="info")
self.log_gui.log(f" 数据范围: {data_range}", level="info")
self.log_gui.log(f" 编码位深: {bit_depth}", level="info")
success = self.ucd.set_sdr_format(
color_space=color_space,
gamma=gamma,
data_range=data_range,
bit_depth=bit_depth,
)
if success:
self.log_gui.log("SDR 信号格式设置成功", level="success")
else:
self.log_gui.log("SDR 信号格式设置失败", level="error")
# 设置图案参数
if mode == "accuracy":
self.log_gui.log(f"设置 SDR 29色色准测试图案...", level="info")
else:
self.log_gui.log(f"设置 SDR 测试图案({mode} 模式)...", level="info")
# ========== ✅✅修改:使用临时配置对象 ==========
import copy
# 从原始配置获取参数(每次都是干净的)
if mode == "rgb":
original_params = copy.deepcopy(
self.config.default_pattern_rgb["pattern_params"]
)
elif mode == "gray":
original_params = copy.deepcopy(
self.config.default_pattern_gray["pattern_params"]
)
elif mode == "accuracy":
original_params = copy.deepcopy(
self.config.default_pattern_accuracy["pattern_params"]
)
elif mode == "custom":
original_params = copy.deepcopy(
self.config.default_pattern_temp["pattern_params"]
)
self.log_gui.log(f" 使用原始 RGB 参数(前 3 个):", level="info")
for i in range(min(3, len(original_params))):
self.log_gui.log(f" [{i+1}] {original_params[i]}", level="info")
# 根据 data_range 转换
converted_params = convert_pattern_params(
pattern_params=original_params, data_range=data_range, verbose=False
)
if data_range == "Limited":
self.log_gui.log(" 转换为 Limited Range (16-235):", level="info")
for i in range(min(3, len(converted_params))):
self.log_gui.log(
f" {original_params[i]}{converted_params[i]}"
, level="info")
else:
self.log_gui.log("Full RangeRGB 保持不变", level="success")
# 创建临时配置对象(不修改 self.config
temp_config = self.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
# 使用临时配置设置参数
self.ucd.set_ucd_params(temp_config)
self.log_gui.log(f"图案参数已设置,共 {len(converted_params)} 个图案", level="success")
# ========== 修改结束 ==========
elif test_type == "hdr_movie":
# HDR 测试设置色彩空间、Metadata 等
self.log_gui.log("=" * 50, level="separator")
self.log_gui.log("设置 HDR 信号格式:", level="info")
self.log_gui.log("=" * 50, level="separator")
color_space = self.hdr_color_space_var.get()
data_range = self.hdr_data_range_var.get()
bit_depth = self.hdr_bit_depth_var.get()
max_cll = self.hdr_maxcll_var.get()
max_fall = self.hdr_maxfall_var.get()
self.log_gui.log(f" 色彩空间: {color_space}", level="info")
self.log_gui.log(f" 数据范围: {data_range}", level="info")
self.log_gui.log(f" 编码位深: {bit_depth}", level="info")
self.log_gui.log(f" MaxCLL: {max_cll}", level="info")
self.log_gui.log(f" MaxFALL: {max_fall}", level="info")
success = self.ucd.set_hdr_format(
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
max_cll=max_cll,
max_fall=max_fall,
)
if success:
self.log_gui.log("HDR 信号格式设置成功", level="success")
else:
self.log_gui.log("HDR 信号格式设置失败", level="error")
# 设置图案参数
if mode == "accuracy":
self.log_gui.log(f"设置 HDR 29色色准测试图案...", level="info")
else:
self.log_gui.log(f"设置 HDR 测试图案({mode} 模式)...", level="info")
# ========== ✅✅修改:使用临时配置对象 ==========
import copy
# 从原始配置获取参数
if mode == "rgb":
original_params = copy.deepcopy(
self.config.default_pattern_rgb["pattern_params"]
)
elif mode == "gray":
original_params = copy.deepcopy(
self.config.default_pattern_gray["pattern_params"]
)
elif mode == "accuracy":
original_params = copy.deepcopy(
self.config.default_pattern_accuracy["pattern_params"]
)
self.log_gui.log(f" 使用原始 RGB 参数(前 3 个):", level="info")
for i in range(min(3, len(original_params))):
self.log_gui.log(f" [{i+1}] {original_params[i]}", level="info")
# 根据 data_range 转换
converted_params = convert_pattern_params(
pattern_params=original_params, data_range=data_range, verbose=False
)
if data_range == "Limited":
self.log_gui.log(" 转换为 Limited Range (16-235):", level="info")
for i in range(min(3, len(converted_params))):
self.log_gui.log(
f" {original_params[i]}{converted_params[i]}"
, level="info")
else:
self.log_gui.log("Full RangeRGB 保持不变", level="success")
# 创建临时配置对象
temp_config = self.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
self.ucd.set_ucd_params(temp_config)
self.log_gui.log(f"图案参数已设置,共 {len(converted_params)} 个图案", level="success")
# ========== 修改结束 ==========
self.log_gui.log("=" * 50, level="separator") self.log_gui.log("=" * 50, level="separator")
# 4. 循环发送图案并采集数据(使用原始配置的数量) # 4. 循环发送图案并采集数据
total_patterns = len(self.config.current_pattern["pattern_params"]) total_patterns = session.total_patterns
self.log_gui.log(f"开始采集数据,共 {total_patterns} 个图案", level="info") self.log_gui.log(f"开始采集数据,共 {total_patterns} 个图案", level="info")
settle_time = max(0.2, float(getattr(self, "pattern_settle_time", 1.0))) settle_time = max(0.2, float(getattr(self, "pattern_settle_time", 1.0)))
progress_step = max( progress_step = max(
@@ -518,14 +324,7 @@ def send_fix_pattern(self, mode):
f"采集等待时间: {settle_time:.2f}s可通过 pattern_settle_time 调整)" f"采集等待时间: {settle_time:.2f}s可通过 pattern_settle_time 调整)"
, level="info") , level="info")
# 获取颜色名称列表(用于日志显示) display_names = session.display_names
color_names = None
if mode == "accuracy":
color_names = self.config.get_accuracy_color_names()
custom_pattern_names = []
if mode == "custom" and hasattr(self.config, "get_temp_pattern_names"):
custom_pattern_names = self.config.get_temp_pattern_names()
for i in range(total_patterns): for i in range(total_patterns):
if not self.testing: if not self.testing:
@@ -538,17 +337,15 @@ def send_fix_pattern(self, mode):
or ((i + 1) % progress_step == 0) or ((i + 1) % progress_step == 0)
) )
# 设置下一个图案(显示颜色名称)
if should_log_detail: if should_log_detail:
if color_names and i < len(color_names): if display_names and i < len(display_names):
self.log_gui.log( self.log_gui.log(
f"发送第 {i+1}/{total_patterns} 个图案: {color_names[i]}..." f"发送第 {i+1}/{total_patterns} 个图案: {display_names[i]}..."
, level="info") , level="info")
else: else:
self.log_gui.log(f"发送第 {i+1}/{total_patterns} 个图案...", level="info") self.log_gui.log(f"发送第 {i+1}/{total_patterns} 个图案...", level="info")
self.ucd.set_next_pattern() self.pattern_service.send_session_pattern(session, i)
self.ucd.run()
time.sleep(settle_time) time.sleep(settle_time)
# 测量数据 # 测量数据
@@ -582,8 +379,8 @@ def send_fix_pattern(self, mode):
) )
row_data = { row_data = {
"pattern_name": ( "pattern_name": (
custom_pattern_names[i] display_names[i]
if i < len(custom_pattern_names) if i < len(display_names)
else f"P {i + 1}" else f"P {i + 1}"
), ),
"X": X, "X": X,

View File

@@ -0,0 +1 @@
from app.services.pattern_service import PatternService, PatternSession

View File

@@ -0,0 +1,199 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
from app.data_range_converter import convert_pattern_params
from drivers.ucd_helpers import send_solid_rgb_pattern
@dataclass
class PatternSession:
mode: str
test_type: str
active_config: object
pattern_params: list[list[int]]
total_patterns: int
display_names: list[str]
class PatternService:
def __init__(self, app):
self.app = app
def prepare_session(self, mode, *, test_type=None, log_details=False):
test_type = test_type or self.app.config.current_test_type
if not self.app.config.set_current_pattern(mode):
raise ValueError(f"未知的图案模式: {mode}")
active_config = self.app.config
source_params = self._get_source_pattern_params(mode)
if test_type == "screen_module":
if log_details:
self._log("=" * 50, "separator")
self._log("设置屏模组信号格式:", "info")
self._log("=" * 50, "separator")
self._log(
f" Timing: {self.app.config.current_test_types[test_type]['timing']}",
"info",
)
self.app.ucd.set_ucd_params(active_config)
elif test_type == "sdr_movie":
active_config = self._prepare_video_session(
mode=mode,
test_type=test_type,
source_params=source_params,
data_range=self.app.sdr_data_range_var.get(),
log_title="设置 SDR 信号格式:",
setup_message=f"设置 SDR 测试图案({mode} 模式)..."
if mode != "accuracy"
else "设置 SDR 29色色准测试图案...",
setup_format=lambda: self.app.ucd.set_sdr_format(
color_space=self.app.sdr_color_space_var.get(),
gamma=self.app.sdr_gamma_type_var.get(),
data_range=self.app.sdr_data_range_var.get(),
bit_depth=self.app.sdr_bit_depth_var.get(),
),
log_items=[
("色彩空间", self.app.sdr_color_space_var.get()),
("Gamma", self.app.sdr_gamma_type_var.get()),
("数据范围", self.app.sdr_data_range_var.get()),
("编码位深", self.app.sdr_bit_depth_var.get()),
],
log_details=log_details,
)
elif test_type == "hdr_movie":
active_config = self._prepare_video_session(
mode=mode,
test_type=test_type,
source_params=source_params,
data_range=self.app.hdr_data_range_var.get(),
log_title="设置 HDR 信号格式:",
setup_message=f"设置 HDR 测试图案({mode} 模式)..."
if mode != "accuracy"
else "设置 HDR 29色色准测试图案...",
setup_format=lambda: self.app.ucd.set_hdr_format(
color_space=self.app.hdr_color_space_var.get(),
data_range=self.app.hdr_data_range_var.get(),
bit_depth=self.app.hdr_bit_depth_var.get(),
max_cll=self.app.hdr_maxcll_var.get(),
max_fall=self.app.hdr_maxfall_var.get(),
),
log_items=[
("色彩空间", self.app.hdr_color_space_var.get()),
("数据范围", self.app.hdr_data_range_var.get()),
("编码位深", self.app.hdr_bit_depth_var.get()),
("MaxCLL", self.app.hdr_maxcll_var.get()),
("MaxFALL", self.app.hdr_maxfall_var.get()),
],
log_details=log_details,
)
else:
raise ValueError(f"不支持的测试类型: {test_type}")
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
return PatternSession(
mode=mode,
test_type=test_type,
active_config=active_config,
pattern_params=pattern_params,
total_patterns=len(pattern_params),
display_names=self._get_display_names(mode, len(pattern_params)),
)
def send_session_pattern(self, session, index):
if index < 0 or index >= session.total_patterns:
raise IndexError(f"pattern 索引越界: {index}")
pattern_param = session.pattern_params[index]
if not self.app.ucd.send_current_pattern_params(pattern_param):
raise RuntimeError(f"发送 pattern 失败: {index}")
return pattern_param
def send_rgb(self, rgb, *, session=None, test_type=None):
active_session = session or self.prepare_session(
"rgb",
test_type=test_type,
log_details=False,
)
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
send_solid_rgb_pattern(self.app.ucd, converted_rgb, raise_on_error=True)
return True
def _prepare_video_session(
self,
*,
mode,
test_type,
source_params,
data_range,
log_title,
setup_message,
setup_format,
log_items,
log_details,
):
if log_details:
self._log("=" * 50, "separator")
self._log(log_title, "info")
self._log("=" * 50, "separator")
for label, value in log_items:
self._log(f" {label}: {value}", "info")
success = setup_format()
if log_details:
self._log(
f"{test_type.split('_')[0].upper()} 信号格式设置{'成功' if success else '失败'}",
"success" if success else "error",
)
self._log(setup_message, "info")
converted_params = convert_pattern_params(
pattern_params=source_params,
data_range=data_range,
verbose=False,
)
active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode,
converted_params=converted_params,
)
self.app.ucd.set_ucd_params(active_config)
if log_details:
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
return active_config
def _get_source_pattern_params(self, mode):
config = self.app.config
if mode == "rgb":
return copy.deepcopy(config.default_pattern_rgb["pattern_params"])
if mode == "gray":
return copy.deepcopy(config.default_pattern_gray["pattern_params"])
if mode == "accuracy":
return copy.deepcopy(config.default_pattern_accuracy["pattern_params"])
if mode == "custom":
return copy.deepcopy(config.default_pattern_temp["pattern_params"])
raise ValueError(f"未知的图案模式: {mode}")
def _get_display_names(self, mode, total_patterns):
if mode == "accuracy":
return self.app.config.get_accuracy_color_names()
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
return self.app.config.get_temp_pattern_names()
return [f"P {index + 1}" for index in range(total_patterns)]
def _convert_rgb_for_test_type(self, rgb, test_type):
if test_type == "sdr_movie":
data_range = self.app.sdr_data_range_var.get()
elif test_type == "hdr_movie":
data_range = self.app.hdr_data_range_var.get()
else:
data_range = "Full"
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
def _log(self, message, level):
if hasattr(self.app, "log_gui"):
self.app.log_gui.log(message, level=level)

View File

@@ -435,14 +435,14 @@ def create_test_type_frame(self):
) )
self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1) self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1)
self.single_step_btn = ttk.Button( # self.single_step_btn = ttk.Button(
self.sidebar_frame, # self.sidebar_frame,
text="单步调试", # text="单步调试",
style="Sidebar.TButton", # style="Sidebar.TButton",
command=self.toggle_single_step_panel, # command=self.toggle_single_step_panel,
takefocus=False, # takefocus=False,
) # )
self.single_step_btn.pack(fill=tk.X, padx=0, pady=1) # self.single_step_btn.pack(fill=tk.X, padx=0, pady=1)
self.pantone_baseline_btn = ttk.Button( self.pantone_baseline_btn = ttk.Button(
self.sidebar_frame, self.sidebar_frame,

View File

@@ -2,23 +2,16 @@
from __future__ import annotations from __future__ import annotations
import csv
import datetime import datetime
import os import os
import tempfile
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
import ttkbootstrap as ttk import ttkbootstrap as ttk
from PIL import Image
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
_PATTERN_FILE = "pantone_patterns_2670.csv"
_TEMPLATE_FILE = "pantone\xa02670\xa0colors.xlsx" _TEMPLATE_FILE = "pantone\xa02670\xa0colors.xlsx"
_TARGET_RESULT_COUNT = 2670
def create_pantone_baseline_panel(self): def create_pantone_baseline_panel(self):
@@ -34,6 +27,7 @@ def create_pantone_baseline_panel(self):
self._pantone_pause_requested = False self._pantone_pause_requested = False
self._pantone_stop_requested = False self._pantone_stop_requested = False
self._pantone_next_index = 0 self._pantone_next_index = 0
self._pantone_target_count = 0
root = ttk.Frame(frame, padding=10) root = ttk.Frame(frame, padding=10)
root.pack(fill=tk.BOTH, expand=True) root.pack(fill=tk.BOTH, expand=True)
@@ -47,12 +41,12 @@ def create_pantone_baseline_panel(self):
).pack(side=tk.LEFT) ).pack(side=tk.LEFT)
self.pantone_status_var = tk.StringVar(value="未开始") self.pantone_status_var = tk.StringVar(value="未开始")
self.pantone_progress_var = tk.StringVar(value="0 / 2670") self.pantone_progress_var = tk.StringVar(value="0 / 0")
self.pantone_settle_var = tk.StringVar(value="0.35") self.pantone_settle_var = tk.StringVar(value="0.35")
config_row = ttk.LabelFrame(root, text="任务配置", padding=8) config_row = ttk.LabelFrame(root, text="任务配置", padding=8)
config_row.pack(fill=tk.X) config_row.pack(fill=tk.X)
ttk.Label(config_row, text=f"Pattern来源: settings/{_PATTERN_FILE}").pack( ttk.Label(config_row, text=f"Pattern来源: settings/{_TEMPLATE_FILE}").pack(
side=tk.LEFT side=tk.LEFT
) )
ttk.Label(config_row, text="稳定等待(s):").pack(side=tk.LEFT, padx=(14, 4)) ttk.Label(config_row, text="稳定等待(s):").pack(side=tk.LEFT, padx=(14, 4))
@@ -160,38 +154,38 @@ def toggle_pantone_baseline_panel(self):
def _load_patterns(self): def _load_patterns(self):
path = os.path.join("settings", _PATTERN_FILE) path = os.path.join("settings", _TEMPLATE_FILE)
if not os.path.isfile(path): if not os.path.isfile(path):
raise FileNotFoundError(f"未找到 pattern 文件: {path}") raise FileNotFoundError(f"未找到模板文件: {path}")
from openpyxl import load_workbook
patterns = [] patterns = []
with open(path, "r", encoding="utf-8-sig", newline="") as fp: wb = load_workbook(path, read_only=True, data_only=True)
reader = csv.DictReader(fp) ws = wb.active
for row in reader: try:
for row in ws.iter_rows(min_row=2, values_only=True):
if not row:
continue
try: try:
r = int(row.get("R", "").strip()) r = int(row[0]) if row[0] is not None else None
g = int(row.get("G", "").strip()) g = int(row[1]) if len(row) > 1 and row[1] is not None else None
b = int(row.get("B", "").strip()) b = int(row[2]) if len(row) > 2 and row[2] is not None else None
except Exception: except Exception:
continue continue
if r is None or g is None or b is None:
continue
if min(r, g, b) < 0 or max(r, g, b) > 255: if min(r, g, b) < 0 or max(r, g, b) > 255:
continue continue
patterns.append((r, g, b)) patterns.append((r, g, b))
finally:
wb.close()
if not patterns: if not patterns:
raise RuntimeError("pattern 文件为空或格式不正确,需包含 R,G,B") raise RuntimeError("模板中未找到有效 RGB 列表(需包含 R/G/B 三列)")
return patterns return patterns
def _build_temp_patch(self, rgb):
width, height = get_current_resolution(self.ucd)
temp_dir = os.path.join(tempfile.gettempdir(), "pq_pantone_baseline")
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(temp_dir, "pantone_current_patch.png")
Image.new("RGB", (width, height), rgb).save(file_path, format="PNG")
return file_path
def _start_pantone_baseline(self): def _start_pantone_baseline(self):
if self._pantone_running: if self._pantone_running:
messagebox.showinfo("提示", "Pantone 任务正在执行") messagebox.showinfo("提示", "Pantone 任务正在执行")
@@ -213,6 +207,7 @@ def _start_pantone_baseline(self):
try: try:
self.pantone_patterns = _load_patterns(self) self.pantone_patterns = _load_patterns(self)
self._pantone_target_count = len(self.pantone_patterns)
except Exception as exc: except Exception as exc:
messagebox.showerror("读取失败", str(exc)) messagebox.showerror("读取失败", str(exc))
return return
@@ -228,7 +223,7 @@ def _start_pantone_baseline(self):
self._pantone_control_event = threading.Event() self._pantone_control_event = threading.Event()
self._pantone_next_index = 0 self._pantone_next_index = 0
self.pantone_status_var.set("执行中") self.pantone_status_var.set("执行中")
self.pantone_progress_var.set(f"0 / {_TARGET_RESULT_COUNT}") self.pantone_progress_var.set(f"0 / {self._pantone_target_count}")
self.pantone_results = [] self.pantone_results = []
for item in self.pantone_tree.get_children(): for item in self.pantone_tree.get_children():
self.pantone_tree.delete(item) self.pantone_tree.delete(item)
@@ -244,9 +239,6 @@ def _resume_pantone_baseline(self):
if not self._pantone_paused: if not self._pantone_paused:
messagebox.showinfo("提示", "当前没有可继续的暂停任务") messagebox.showinfo("提示", "当前没有可继续的暂停任务")
return return
if self._pantone_next_index >= _TARGET_RESULT_COUNT:
messagebox.showinfo("提示", "任务已完成,无需继续")
return
if not getattr(self, "ucd", None) or not self.ucd.status: if not getattr(self, "ucd", None) or not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323") messagebox.showwarning("警告", "请先连接 UCD323")
return return
@@ -264,10 +256,15 @@ def _resume_pantone_baseline(self):
try: try:
self.pantone_patterns = _load_patterns(self) self.pantone_patterns = _load_patterns(self)
self._pantone_target_count = len(self.pantone_patterns)
except Exception as exc: except Exception as exc:
messagebox.showerror("读取失败", str(exc)) messagebox.showerror("读取失败", str(exc))
return return
if self._pantone_next_index >= self._pantone_target_count:
messagebox.showinfo("提示", "任务已完成,无需继续")
return
self._pantone_running = True self._pantone_running = True
self._pantone_paused = False self._pantone_paused = False
self._pantone_pause_requested = False self._pantone_pause_requested = False
@@ -280,13 +277,14 @@ def _resume_pantone_baseline(self):
def _launch_worker(self, start_index, settle): def _launch_worker(self, start_index, settle):
total = _TARGET_RESULT_COUNT total = self._pantone_target_count or len(self.pantone_patterns)
def worker(): def worker():
end_state = "completed" end_state = "completed"
try: try:
src = self.pantone_patterns src = self.pantone_patterns
src_count = len(src) src_count = len(src)
rgb_session = self.pattern_service.prepare_session("rgb", log_details=False)
self._dispatch_ui( self._dispatch_ui(
self.log_gui.log, self.log_gui.log,
f"Pantone 认证摸底启动: source={src_count}, target={total}, start={start_index + 1}", f"Pantone 认证摸底启动: source={src_count}, target={total}, start={start_index + 1}",
@@ -301,9 +299,10 @@ def _launch_worker(self, start_index, settle):
break break
r, g, b = src[i % src_count] r, g, b = src[i % src_count]
image_path = _build_temp_patch(self, (r, g, b)) try:
if not send_image_pattern(self.ucd, image_path): self.pattern_service.send_rgb((r, g, b), session=rgb_session)
raise RuntimeError(f"{i + 1} 组发送失败") except Exception as exc:
raise RuntimeError(f"{i + 1} 组发送失败: {exc}") from exc
if settle > 0 and self._pantone_control_event is not None: if settle > 0 and self._pantone_control_event is not None:
self._pantone_control_event.clear() self._pantone_control_event.clear()
@@ -360,6 +359,19 @@ def _launch_worker(self, start_index, settle):
f"Pantone 任务完成,共 {len(self.pantone_results)} 条数据", f"Pantone 任务完成,共 {len(self.pantone_results)} 条数据",
"success", "success",
) )
try:
auto_path = _auto_save_template(self)
self._dispatch_ui(
self.log_gui.log,
f"Pantone 模板已自动保存: {auto_path}",
"success",
)
except Exception as exc:
self._dispatch_ui(
self.log_gui.log,
f"Pantone 自动保存模板失败: {exc}",
"error",
)
except Exception as exc: except Exception as exc:
self._pantone_paused = False self._pantone_paused = False
self._dispatch_ui(self.pantone_status_var.set, "执行失败") self._dispatch_ui(self.pantone_status_var.set, "执行失败")
@@ -430,7 +442,8 @@ def _clear_results(self):
self._pantone_next_index = 0 self._pantone_next_index = 0
for item in self.pantone_tree.get_children(): for item in self.pantone_tree.get_children():
self.pantone_tree.delete(item) self.pantone_tree.delete(item)
self.pantone_progress_var.set(f"0 / {_TARGET_RESULT_COUNT}") self._pantone_target_count = 0
self.pantone_progress_var.set("0 / 0")
self.pantone_status_var.set("结果已清空") self.pantone_status_var.set("结果已清空")
_set_button_states(self) _set_button_states(self)
@@ -447,7 +460,7 @@ def _set_button_states(self):
self.pantone_pause_btn.configure(state=tk.DISABLED) self.pantone_pause_btn.configure(state=tk.DISABLED)
self.pantone_end_btn.configure(state=tk.NORMAL if (self._pantone_paused or self.pantone_results) else tk.DISABLED) self.pantone_end_btn.configure(state=tk.NORMAL if (self._pantone_paused or self.pantone_results) else tk.DISABLED)
can_resume = self._pantone_paused and self._pantone_next_index < _TARGET_RESULT_COUNT can_resume = self._pantone_paused and self._pantone_next_index < self._pantone_target_count
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED) self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
@@ -456,7 +469,7 @@ def _save_as_template(self):
messagebox.showinfo("提示", "暂无可导出的结果") messagebox.showinfo("提示", "暂无可导出的结果")
return return
default_name = "pantone 2670 colors.xlsx" default_name = _TEMPLATE_FILE.replace("\xa0", " ")
path = filedialog.asksaveasfilename( path = filedialog.asksaveasfilename(
title="另存为 Pantone 模板", title="另存为 Pantone 模板",
defaultextension=".xlsx", defaultextension=".xlsx",
@@ -466,41 +479,69 @@ def _save_as_template(self):
if not path: if not path:
return return
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
template_path = os.path.join("settings", _TEMPLATE_FILE)
try: try:
from openpyxl import load_workbook, Workbook _write_template_xlsx(self, path)
if os.path.isfile(template_path):
wb = load_workbook(template_path)
ws = wb.active
else:
wb = Workbook()
ws = wb.active
ws.title = "Sheet1"
ws.cell(row=1, column=1, value="R")
ws.cell(row=1, column=2, value="G")
ws.cell(row=1, column=3, value="B")
ws.cell(row=1, column=4, value="L")
ws.cell(row=1, column=5, value="x")
ws.cell(row=1, column=6, value="y")
# 清空旧数据
max_row = max(ws.max_row, 2)
for row in range(2, max_row + 1):
for col in range(1, 7):
ws.cell(row=row, column=col, value=None)
for idx, item in enumerate(self.pantone_results, start=2):
ws.cell(row=idx, column=1, value=int(item["r"]))
ws.cell(row=idx, column=2, value=int(item["g"]))
ws.cell(row=idx, column=3, value=int(item["b"]))
ws.cell(row=idx, column=4, value=float(item["l"]))
ws.cell(row=idx, column=5, value=float(item["x"]))
ws.cell(row=idx, column=6, value=float(item["y"]))
wb.save(path)
self.log_gui.log(f"Pantone 模板已保存: {path}", level="success") self.log_gui.log(f"Pantone 模板已保存: {path}", level="success")
self.pantone_status_var.set(f"已保存: {os.path.basename(path)}") self.pantone_status_var.set(f"已保存: {os.path.basename(path)}")
except Exception as exc: except Exception as exc:
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}") messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")
def _resolve_results_dir(self):
if getattr(self, "config_file", None):
root_dir = os.path.dirname(os.path.dirname(self.config_file))
else:
root_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
results_dir = os.path.join(root_dir, "results")
os.makedirs(results_dir, exist_ok=True)
return results_dir
def _auto_save_template(self):
results_dir = _resolve_results_dir(self)
target_count = len(self.pantone_results)
filename = (
f"pantone_{target_count}_baseline_"
f"{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
)
path = os.path.join(results_dir, filename)
_write_template_xlsx(self, path)
return path
def _write_template_xlsx(self, path):
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
template_path = os.path.join("settings", _TEMPLATE_FILE)
from openpyxl import load_workbook, Workbook
if os.path.isfile(template_path):
wb = load_workbook(template_path)
ws = wb.active
else:
wb = Workbook()
ws = wb.active
ws.title = "Sheet1"
ws.cell(row=1, column=1, value="R")
ws.cell(row=1, column=2, value="G")
ws.cell(row=1, column=3, value="B")
ws.cell(row=1, column=4, value="L")
ws.cell(row=1, column=5, value="x")
ws.cell(row=1, column=6, value="y")
# 清空旧数据
max_row = max(ws.max_row, 2)
for row in range(2, max_row + 1):
for col in range(1, 7):
ws.cell(row=row, column=col, value=None)
for idx, item in enumerate(self.pantone_results, start=2):
ws.cell(row=idx, column=1, value=int(item["r"]))
ws.cell(row=idx, column=2, value=int(item["g"]))
ws.cell(row=idx, column=3, value=int(item["b"]))
ws.cell(row=idx, column=4, value=float(item["l"]))
ws.cell(row=idx, column=5, value=float(item["x"]))
ws.cell(row=idx, column=6, value=float(item["y"]))
wb.save(path)

View File

@@ -768,28 +768,25 @@ class PQDebugPanel:
# 禁用按钮 # 禁用按钮
self._disable_test_button(test_type, test_item) self._disable_test_button(test_type, test_item)
# 根据测试类型设置信号格式
self._setup_signal_format(test_type)
# 获取图案索引并发送 # 获取图案索引并发送
if test_item in ["gamma", "eotf"]: if test_item in ["gamma", "eotf"]:
pattern_index = self.get_gray_index(selected) pattern_index = self.get_gray_index(selected)
self.app.config.set_current_pattern("gray") pattern_mode = "gray"
elif test_item == "accuracy": elif test_item == "accuracy":
pattern_index = self.get_color_index(selected) pattern_index = self.get_color_index(selected)
self.app.config.set_current_pattern("accuracy") pattern_mode = "accuracy"
elif test_item == "rgb": elif test_item == "rgb":
pattern_index = self.get_rgb_index(selected) pattern_index = self.get_rgb_index(selected)
self.app.config.set_current_pattern("rgb") pattern_mode = "rgb"
else:
raise ValueError(f"不支持的测试项目: {test_item}")
# 设置图案 session = self.app.pattern_service.prepare_session(
self.app.ucd.set_ucd_params(self.app.config) pattern_mode,
test_type=test_type,
# 跳转到目标图案 log_details=False,
for i in range(pattern_index + 1): )
self.app.ucd.set_next_pattern() self.app.pattern_service.send_session_pattern(session, pattern_index)
self.app.ucd.run()
time.sleep(1.5) time.sleep(1.5)
# 测量数据 # 测量数据
@@ -815,28 +812,6 @@ class PQDebugPanel:
self.app.log_gui.log(traceback.format_exc(), level="error") self.app.log_gui.log(traceback.format_exc(), level="error")
self._enable_test_button(test_type, test_item) self._enable_test_button(test_type, test_item)
def _setup_signal_format(self, test_type):
"""设置信号格式"""
if test_type == "screen_module":
self.app.ucd.set_ucd_params(self.app.config)
elif test_type == "sdr_movie":
self.app.ucd.set_sdr_format(
color_space=self.app.sdr_color_space_var.get(),
gamma=self.app.sdr_gamma_type_var.get(),
data_range=self.app.sdr_data_range_var.get(),
bit_depth=self.app.sdr_bit_depth_var.get(),
)
elif test_type == "hdr_movie":
self.app.ucd.set_hdr_format(
color_space=self.app.hdr_color_space_var.get(),
data_range=self.app.hdr_data_range_var.get(),
bit_depth=self.app.hdr_bit_depth_var.get(),
max_cll=self.app.hdr_maxcll_var.get(),
max_fall=self.app.hdr_maxfall_var.get(),
)
def _compare_and_display(self, test_type, test_item, selected, new_data): def _compare_and_display(self, test_type, test_item, selected, new_data):
"""对比数据并显示""" """对比数据并显示"""
# 获取原始数据 # 获取原始数据

View File

@@ -1,5 +1,5 @@
APP_NAME = "PQ 自动化测试工具" APP_NAME = "PQ 自动化测试工具"
APP_VERSION = "1.1.0" APP_VERSION = "0.1.0"
def get_app_title(): def get_app_title():

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ class UCDController:
self.config = None self.config = None
self.color_mode = None self.color_mode = None
self.status = False self.status = False
self.current_interface = "HDMI"
self.current_timing = None self.current_timing = None
self.current_pattern = None self.current_pattern = None
@@ -42,12 +43,14 @@ class UCDController:
try: try:
self.role = temp_dev.select_role(UniTAP.dev.UCD323.HDMISource) self.role = temp_dev.select_role(UniTAP.dev.UCD323.HDMISource)
self.dev = temp_dev self.dev = temp_dev
self.current_interface = "HDMI"
except Exception as role_error: except Exception as role_error:
self._close_device_object(temp_dev) self._close_device_object(temp_dev)
raise role_error raise role_error
self.timing_manager = self.role.hdtx.pg.timing_manager pg, _ = self.get_tx_modules()
self.timing_manager = pg.timing_manager
self.color_mode = UniTAP.ColorInfo() self.color_mode = UniTAP.ColorInfo()
self.status = True self.status = True
@@ -72,6 +75,7 @@ class UCDController:
self.current_pattern_param = None self.current_pattern_param = None
self.current_pattern_params = None self.current_pattern_params = None
self.current_pattern_index = 0 self.current_pattern_index = 0
self.current_interface = "HDMI"
self.lUniTAP = None self.lUniTAP = None
@@ -94,6 +98,7 @@ class UCDController:
self.current_pattern_param = None self.current_pattern_param = None
self.current_pattern_params = None self.current_pattern_params = None
self.current_pattern_index = 0 self.current_pattern_index = 0
self.current_interface = "HDMI"
try: try:
self.lUniTAP = None self.lUniTAP = None
@@ -139,10 +144,51 @@ class UCDController:
self.current_pattern_param = None self.current_pattern_param = None
self.current_pattern_params = None self.current_pattern_params = None
self.current_pattern_index = 0 self.current_pattern_index = 0
self.current_interface = "HDMI"
except Exception as e: except Exception as e:
pass pass
def get_tx_modules(self):
"""根据当前接口返回 (pg, ag) 模块。"""
if not self.role:
raise RuntimeError("UCD 未打开,无法获取 TX 模块")
interface = getattr(self, "current_interface", None)
if interface in (None, "HDMI"):
return self.role.hdtx.pg, self.role.hdtx.ag
if interface in ("DP", "Type-C"):
return self.role.dptx.pg, self.role.dptx.ag
raise ValueError(f"不支持的接口类型: {interface}")
def _resolve_timing(self, pg=None):
"""优先从 current_timing 读取 timing必要时回退到 TX 模块。"""
if self.current_timing is not None:
return self.current_timing
if pg is not None:
get_vm = getattr(pg, "get_vm", None)
if callable(get_vm):
try:
vm = get_vm()
return getattr(vm, "timing", None)
except Exception:
pass
return None
def get_current_resolution(self, default=(3840, 2160)):
"""从当前 timing 获取 (width, height),失败时返回 default。"""
try:
pg, _ = self.get_tx_modules()
timing = self._resolve_timing(pg)
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
return timing.h_active, timing.v_active
except Exception:
pass
return default
def _cleanup(self): def _cleanup(self):
"""清理设备资源""" """清理设备资源"""
try: try:
@@ -161,6 +207,9 @@ class UCDController:
self.config = config self.config = config
test_type = self.config.current_test_type test_type = self.config.current_test_type
pg, _ = self.get_tx_modules()
self.timing_manager = pg.timing_manager
if test_type == "hdr_movie": if test_type == "hdr_movie":
color_format_str = self.config.current_test_types[test_type]["color_format"] color_format_str = self.config.current_test_types[test_type]["color_format"]
color_format = UCDEnum.ColorInfo.get_color_format(color_format_str) color_format = UCDEnum.ColorInfo.get_color_format(color_format_str)
@@ -197,9 +246,58 @@ class UCDController:
"""运行设备""" """运行设备"""
self.apply_video_mode() self.apply_video_mode()
self.apply_pattern() self.apply_pattern()
self.role.hdtx.pg.apply() pg, _ = self.get_tx_modules()
pg.apply()
return True return True
def send_image_pattern(self, image_path):
"""发送图片 Pattern依赖当前 timing/color_mode 状态)。"""
if not self.status or not self.role:
return False
try:
pg, _ = self.get_tx_modules()
self.apply_video_mode()
pg.set_pattern(pattern=image_path)
pg.apply()
return True
except Exception:
return False
def send_solid_rgb_pattern(self, rgb):
"""发送纯色 RGB Pattern依赖当前 timing/color_mode 状态)。"""
if not self.status or not self.role:
return False
try:
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if self.current_pattern is None:
return False
return self.send_current_pattern_params(list(rgb))
except Exception:
return False
def send_current_pattern_params(self, pattern_params):
"""发送当前已配置的 pattern并可附带当前 pattern 参数。"""
if not self.status or not self.role:
return False
try:
if self.current_pattern is None:
return False
if pattern_params is not None and not self.set_pattern(
self.current_pattern,
pattern_params,
):
return False
self.run()
return True
except Exception:
return False
def set_color_mode(self, cf, bpc, cm): def set_color_mode(self, cf, bpc, cm):
"""设置颜色模式""" """设置颜色模式"""
current_dynamic_range = None current_dynamic_range = None
@@ -246,7 +344,8 @@ class UCDController:
video_mode = UniTAP.VideoMode( video_mode = UniTAP.VideoMode(
timing=self.current_timing, color_info=self.color_mode timing=self.current_timing, color_info=self.color_mode
) )
self.role.hdtx.pg.set_vm(vm=video_mode) pg, _ = self.get_tx_modules()
pg.set_vm(vm=video_mode)
return True return True
def set_pattern(self, pattern, pattern_params=None): def set_pattern(self, pattern, pattern_params=None):
@@ -292,10 +391,11 @@ class UCDController:
def apply_pattern(self): def apply_pattern(self):
"""应用当前pattern""" """应用当前pattern"""
if self.current_pattern: if self.current_pattern:
self.role.hdtx.pg.set_pattern(self.current_pattern) pg, _ = self.get_tx_modules()
pg.set_pattern(self.current_pattern)
if self.current_pattern_param: if self.current_pattern_param:
self.role.hdtx.pg.set_pattern_params(self.current_pattern_param) pg.set_pattern_params(self.current_pattern_param)
return True return True
return False return False

View File

@@ -1,80 +1,46 @@
"""通用 UCD323/UCDController 辅助函数。 """通用 UCD323/UCDController 辅助函数。
封装"按当前接口取 tx 模块""读取分辨率""发送图片 Pattern"等所有 保留为兼容层和薄代理,避免业务模块直接依赖控制器内部实现细节。
测试模块共用的低层 UCD 操作,避免在多个业务模块中重复 if/else。
""" """
import UniTAP
def get_tx_modules(ucd): def get_tx_modules(ucd):
"""根据当前接口返回 (pg, ag) 模块。 """根据当前接口返回 (pg, ag) 模块。"""
return ucd.get_tx_modules()
兼容 UCD323Controller多接口含 current_interface 属性)
与老的 UCDController仅 HDMI
"""
interface = getattr(ucd, "current_interface", None)
if interface in (None, "HDMI"):
return ucd.role.hdtx.pg, ucd.role.hdtx.ag
if interface in ("DP", "Type-C"):
return ucd.role.dptx.pg, ucd.role.dptx.ag
raise ValueError(f"不支持的接口类型: {interface}")
def get_current_resolution(ucd, default=(3840, 2160)): def get_current_resolution(ucd, default=(3840, 2160)):
"""从 UCD 当前 timing 获取 (width, height),失败时返回 default。""" """从 UCD 当前 timing 获取 (width, height),失败时返回 default。"""
try: return ucd.get_current_resolution(default)
pg, _ = get_tx_modules(ucd)
vm = pg.get_vm()
timing = getattr(vm, "timing", None)
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
return timing.h_active, timing.v_active
except Exception:
pass
timing = getattr(ucd, "current_timing", None)
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
return timing.h_active, timing.v_active
return default
def send_image_pattern(ucd, image_path, *, bpc=8, color_format=None, colorimetry=None): def send_image_pattern(ucd, image_path):
"""通过 UCD 发送一张本地图片作为显示 Pattern。 """通过 UCDController 发送一张本地图片作为显示 Pattern。"""
自动停止音频以避免蜂鸣声。颜色参数留空时使用 RGB / sRGB 默认值。
返回 True/False。
"""
if not getattr(ucd, "status", False): if not getattr(ucd, "status", False):
return False return False
try: send_via_controller = getattr(ucd, "send_image_pattern", None)
pg, ag = get_tx_modules(ucd) if not callable(send_via_controller):
except Exception: return False
return bool(send_via_controller(image_path))
def send_solid_rgb_pattern(ucd, rgb, *, raise_on_error=False):
"""通过 UCDController 当前状态发送一组纯色 RGB Pattern。"""
if not getattr(ucd, "status", False):
if raise_on_error:
raise RuntimeError("UCD 未连接,无法发送纯色 Pattern")
return False return False
try: send_via_controller = getattr(ucd, "send_solid_rgb_pattern", None)
ag.stop_generate() if not callable(send_via_controller):
except Exception: if raise_on_error:
pass raise RuntimeError("UCDController 未实现 send_solid_rgb_pattern")
color_mode = UniTAP.ColorInfo()
color_mode.color_format = color_format or UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = bpc
color_mode.colorimetry = colorimetry or UniTAP.ColorInfo.Colorimetry.CM_sRGB
timing = None
try:
vm = pg.get_vm()
timing = getattr(vm, "timing", None)
except Exception:
pass
try:
if timing:
pg.set_vm(vm=UniTAP.VideoMode(timing=timing, color_info=color_mode))
pg.set_pattern(pattern=image_path)
pg.apply()
return True
except Exception:
return False return False
ok = bool(send_via_controller(list(rgb)))
if not ok and raise_on_error:
raise RuntimeError(f"发送纯色 Pattern 失败: {rgb}")
return ok

View File

@@ -74,6 +74,7 @@ from app.tests.local_dimming import (
stop_local_dimming_test as _ld_stop_local_dimming_test, stop_local_dimming_test as _ld_stop_local_dimming_test,
update_ld_results as _ld_update_ld_results, update_ld_results as _ld_update_ld_results,
) )
from app.services import PatternService
from app.device.connection import ( from app.device.connection import (
check_com_connections as _dev_check_com_connections, check_com_connections as _dev_check_com_connections,
check_port_connection as _dev_check_port_connection, check_port_connection as _dev_check_port_connection,
@@ -139,6 +140,7 @@ class PQAutomationApp:
# 创建配置对象 # 创建配置对象
self.config = PQConfig() self.config = PQConfig()
self.pattern_service = PatternService(self)
# 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误 # 结果管理器:按 test_type 保留每次测试结果,始终存在,避免未初始化错误
self.results = PQResultStore() self.results = PQResultStore()

View File

@@ -50,7 +50,7 @@
} }
}, },
"device_config": { "device_config": {
"ca_com": "COM1", "ca_com": "COM3",
"ucd_list": "0: UCD-323 [2128C209]", "ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0" "ca_channel": "0"
}, },