添加信号格式修改

This commit is contained in:
xinzhu.yin
2026-05-22 11:31:36 +08:00
parent c42287b7d7
commit 9b2bc44e17
14 changed files with 434 additions and 356 deletions

View File

@@ -49,6 +49,7 @@ def plot_accuracy(self, accuracy_data, test_type):
fontsize=11,
y=0.98,
fontweight="bold",
color="#111111",
)
# ========== 29色6行5列布局 ==========
@@ -205,6 +206,7 @@ def plot_accuracy(self, accuracy_data, test_type):
va="top",
fontsize=7.5,
fontweight="bold",
color="#111111",
transform=self.accuracy_ax.transAxes,
)
@@ -228,6 +230,7 @@ def plot_accuracy(self, accuracy_data, test_type):
va="center",
fontsize=7,
fontweight="bold",
color="#111111",
transform=self.accuracy_ax.transAxes,
)
@@ -279,11 +282,11 @@ def plot_accuracy(self, accuracy_data, test_type):
grade_color = "darkgreen"
elif avg_delta_e < 3:
grade = "优秀"
grade_icon = "✓✓"
grade_icon = "OK"
grade_color = "green"
elif avg_delta_e < 5:
grade = "良好"
grade_icon = ""
grade_icon = "PASS"
grade_color = "orange"
else:
grade = "需要校准"
@@ -298,6 +301,7 @@ def plot_accuracy(self, accuracy_data, test_type):
va="bottom",
fontsize=7,
fontweight="bold",
color="#111111",
transform=self.accuracy_ax.transAxes,
)

View File

@@ -143,12 +143,10 @@ def run_custom_sdr_test(self, test_items):
self.log_gui.log("执行客户定制 SDR 测试...", level="info")
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
gamma_type = self.sdr_gamma_type_var.get() # 2.2/2.4/2.6
data_range = self.sdr_data_range_var.get() # Full/Limited
bit_depth = self.sdr_bit_depth_var.get() # 8bit/10bit/12bit
self.log_gui.log(f"信号格式: 色彩空间={color_space}, Gamma={gamma_type}", level="info")
self.log_gui.log(f" 数据范围={data_range}, 编码位深={bit_depth}", level="info")
self.log_gui.log(f"信号格式: 色彩空间={color_space}, 数据范围={data_range}, 编码位深={bit_depth}", level="info")
self.log_gui.log("开始统一采集灰阶数据(用于 Gamma/CCT/对比度测试)", level="info")
self.test_custom_sdr()
@@ -168,12 +166,10 @@ def run_sdr_movie_test(self, test_items):
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
gamma_type = self.sdr_gamma_type_var.get() # 2.2/2.4/2.6
data_range = self.sdr_data_range_var.get() # Full/Limited
bit_depth = self.sdr_bit_depth_var.get() # 8bit/10bit/12bit
self.log_gui.log(f"信号格式: 色彩空间={color_space}, Gamma={gamma_type}", level="info")
self.log_gui.log(f" 数据范围={data_range}, 编码位深={bit_depth}", level="info")
self.log_gui.log(f"信号格式: 色彩空间={color_space}, 数据范围={data_range}, 编码位深={bit_depth}", level="info")
# 判断是否需要灰阶数据
needs_gray_data = any(
@@ -313,7 +309,20 @@ def send_fix_pattern(self, mode):
self.log_gui.log("=" * 50, level="separator")
# 4. 循环发送图案并采集数据
# 信号格式设置后等待电视重新锁定 HDMI 信号
# format_changed=True 表示本次 set_video_mode 的参数与上次不同TV 需要重新锁定
format_changed = getattr(getattr(self, "ucd", None), "format_changed", True)
if format_changed:
signal_settle = max(1.0, float(getattr(self, "signal_settle_time", 5.0)))
self.log_gui.log(
f"信号格式已变化,等待电视重新锁定: {signal_settle:.1f}s可通过 signal_settle_time 调整)",
level="info",
)
else:
signal_settle = 0.5
self.log_gui.log("信号格式未变化,短暂等待: 0.5s", level="info")
time.sleep(signal_settle)
total_patterns = session.total_patterns
self.log_gui.log(f"开始采集数据,共 {total_patterns} 个图案", level="info")
settle_time = max(0.2, float(getattr(self, "pattern_settle_time", 1.0)))
@@ -332,7 +341,8 @@ def send_fix_pattern(self, mode):
return results
should_log_detail = (
i == 0
total_patterns <= progress_step
or i == 0
or (i + 1) == total_patterns
or ((i + 1) % progress_step == 0)
)
@@ -477,12 +487,14 @@ def test_gamut(self, test_type):
# SDR 测试:使用色彩空间设置
color_space = self.sdr_color_space_var.get()
if color_space == "BT.709":
if color_space in ("sRGB", "BT.709"):
reference_standard = "BT.709"
elif color_space == "BT.601":
reference_standard = "BT.601"
elif color_space == "BT.2020":
reference_standard = "BT.2020"
elif color_space == "DCI-P3":
reference_standard = "DCI-P3"
else:
reference_standard = "BT.709"
self.log_gui.log(
@@ -665,7 +677,7 @@ def test_gamma(self, test_type, gray_data=None):
"gamma", {"gamma": results_with_gamma_list, "L_bar": L_bar}
)
# 绘制Gamma曲线
# 绘制Gamma曲线SDR 使用用户选择的参考值)
if test_type == "sdr_movie":
try:
target_gamma = float(self.sdr_gamma_type_var.get())
@@ -673,7 +685,6 @@ def test_gamma(self, test_type, gray_data=None):
target_gamma = 2.2
else:
target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
self._save_chart_snapshot(test_type, "gamma", (L_bar, results_with_gamma_list, target_gamma, test_type))
@@ -877,7 +888,7 @@ def test_contrast(self, test_type, gray_data=None):
def test_color_accuracy(self, test_type):
"""测试色准 - 使用手工实现的 ΔE 2000应用 Gamma"""
# ========== 读取用户选择的 Gamma ==========
# ========== Gamma 参考值 ==========
if test_type == "sdr_movie":
try:
target_gamma = float(self.sdr_gamma_type_var.get())
@@ -886,7 +897,7 @@ def test_color_accuracy(self, test_type):
self.log_gui.log("=" * 50, level="separator")
self.log_gui.log(f"开始测试色准SDR Movie 标准 - 29色", level="info")
self.log_gui.log(f"使用 Gamma: {target_gamma}", level="success") # ← 新增
self.log_gui.log(f"使用 Gamma: {target_gamma}", level="success")
self.log_gui.log("=" * 50, level="separator")
elif test_type == "hdr_movie":
@@ -962,9 +973,9 @@ def test_color_accuracy(self, test_type):
color_patches.append(name)
if delta_e < 3:
grade, icon = "优秀", ""
grade, icon = "优秀", "OK"
elif delta_e < 5:
grade, icon = "良好", ""
grade, icon = "良好", "WARN"
else:
grade, icon = "偏差", "[Error]"

View File

@@ -16,6 +16,7 @@ import json
import logging
import mimetypes
import os
import sys
import shutil
import threading
import time
@@ -253,8 +254,13 @@ def _call_pqtest_generate(user_message: str, session_id: str, timeout: float = A
def get_cache_dir(base_dir: Optional[str] = None) -> str:
"""返回缓存目录,如不存在则创建。``base_dir`` 默认使用当前工作目录。"""
root = base_dir if base_dir else os.getcwd()
"""返回缓存目录,如不存在则创建。``base_dir`` 留空时使用应用根目录。"""
if base_dir:
root = base_dir
elif getattr(sys, "frozen", False):
root = os.path.dirname(sys.executable)
else:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
path = os.path.join(root, _CACHE_DIRNAME)
os.makedirs(path, exist_ok=True)
return path

View File

@@ -40,56 +40,72 @@ class PatternService:
"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=[
data_range = self.app.sdr_data_range_var.get()
if log_details:
self._log("=" * 50, "separator")
self._log("设置 SDR 信号格式:", "info")
self._log("=" * 50, "separator")
for label, value in [
("色彩空间", 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_output_format_var.get()),
("Gamma", self.app.sdr_gamma_type_var.get()),
("数据范围", data_range),
("编码位深", self.app.sdr_bit_depth_var.get()),
],
log_details=log_details,
]:
self._log(f" {label}: {value}", "info")
converted_params = convert_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)
success = self.app.ucd.apply_signal_format(
color_space=self.app.sdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.sdr_bit_depth_var.get(),
color_format=self.app.sdr_output_format_var.get(),
)
if log_details:
self._log(f"SDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
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=[
data_range = self.app.hdr_data_range_var.get()
if log_details:
self._log("=" * 50, "separator")
self._log("设置 HDR 信号格式:", "info")
self._log("=" * 50, "separator")
for label, value in [
("色彩空间", self.app.hdr_color_space_var.get()),
("数据范围", self.app.hdr_data_range_var.get()),
("色彩格式", self.app.hdr_output_format_var.get()),
("数据范围", data_range),
("编码位深", self.app.hdr_bit_depth_var.get()),
("MaxCLL", self.app.hdr_maxcll_var.get()),
("MaxCLL", self.app.hdr_maxcll_var.get()),
("MaxFALL", self.app.hdr_maxfall_var.get()),
],
log_details=log_details,
]:
self._log(f" {label}: {value}", "info")
converted_params = convert_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)
success = self.app.ucd.apply_signal_format(
color_space=self.app.hdr_color_space_var.get(),
data_range=data_range,
bit_depth=self.app.hdr_bit_depth_var.get(),
color_format=self.app.hdr_output_format_var.get(),
max_cll=self.app.hdr_maxcll_var.get(),
max_fall=self.app.hdr_maxfall_var.get(),
)
if log_details:
self._log(f"HDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
else:
raise ValueError(f"不支持的测试类型: {test_type}")
@@ -122,50 +138,6 @@ class PatternService:
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):
return copy.deepcopy(get_pattern(mode)["pattern_params"])

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import os
import sys
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
@@ -193,6 +194,15 @@ def toggle_ai_image_panel(self):
self.show_panel("ai_image")
def _get_app_base_dir(self) -> str:
"""返回应用根目录settings 的上一级)。"""
if getattr(self, "config_file", None):
return os.path.dirname(os.path.dirname(self.config_file))
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
# ---------------- 列表 / 选中 ----------------
@@ -203,7 +213,7 @@ def reload_ai_image_list(self, auto_select_first=True):
其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。
auto_select_first: 是否自动选中第一张图片(默认 True
"""
self.ai_image_records = _svc.list_records()
self.ai_image_records = _svc.list_records(base_dir=_get_app_base_dir(self))
self.ai_image_listbox.delete(0, tk.END)
# 维护行号 → 记录索引的映射;分隔头处为 None
self._ai_image_row_map = []
@@ -414,6 +424,7 @@ def _send_prompt(self):
prompt,
on_success=_success,
on_error=_error,
base_dir=_get_app_base_dir(self),
cancel_event=self._ai_image_cancel_event,
)
return
@@ -422,6 +433,7 @@ def _send_prompt(self):
prompt,
on_success=_success,
on_error=_error,
base_dir=_get_app_base_dir(self),
cancel_event=self._ai_image_cancel_event,
)

View File

@@ -137,21 +137,21 @@ def create_signal_format_content(self):
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["BT.709", "BT.601", "BT.2020"],
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma
# Gamma测试参考值用于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")
self.sdr_gamma_type_var = tk.StringVar(value=UCDEnum.SignalFormat.GammaType.GAMMA_22)
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=["2.2", "2.4", "2.6"],
values=UCDEnum.SignalFormat.GammaType.get_list(),
width=10,
state="readonly",
)
@@ -161,11 +161,11 @@ def create_signal_format_content(self):
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")
self.sdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=["Full", "Limited"],
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
@@ -175,16 +175,50 @@ def create_signal_format_content(self):
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")
self.sdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 分辨率
ttk.Label(self.sdr_signal_frame, text="分辨率:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_timing_var = tk.StringVar(
value=self.config.current_test_types.get("sdr_movie", {}).get(
"timing", "DMT 1920x1080@60Hz"
)
)
sdr_timing_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
width=20,
state="readonly",
)
sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed)
sdr_timing_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩格式
ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid(
row=5, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_output_format_var = tk.StringVar(value=UCDEnum.SignalFormat.OutputFormat.RGB)
sdr_output_format_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
sdr_output_format_combo.bind("<<ComboboxSelected>>", self.on_sdr_output_format_changed)
sdr_output_format_combo.grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== HDR信号格式设置 ====================
self.hdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
@@ -235,11 +269,11 @@ def create_signal_format_content(self):
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")
self.hdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
hdr_range_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_data_range_var,
values=["Full", "Limited"],
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
@@ -249,16 +283,31 @@ def create_signal_format_content(self):
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")
self.hdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
hdr_bit_depth_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
hdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩格式
ttk.Label(self.hdr_signal_frame, text="色彩格式:").grid(
row=5, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_output_format_var = tk.StringVar(value=UCDEnum.SignalFormat.OutputFormat.RGB)
hdr_output_format_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_output_format_var,
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
width=10,
state="readonly",
)
hdr_output_format_combo.bind("<<ComboboxSelected>>", self.on_hdr_output_format_changed)
hdr_output_format_combo.grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== 初始化:默认只启用屏模组 Tab ====================
self.signal_tabs.select(0) # 选中屏模组
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
@@ -271,8 +320,9 @@ def create_connection_content(self):
com_frame = ttk.Frame(self.connection_frame)
com_frame.pack(fill=tk.X, pady=5)
# 获取可用的COM端口列表
# 获取可用的COM端口列表和UCD设备列表
available_ports = self.get_available_com_ports()
available_ucd_list = self.get_available_ucd_ports()
# 使用网格布局,更整齐
ttk.Label(com_frame, text="UCD列表:").grid(
@@ -282,7 +332,7 @@ def create_connection_content(self):
self.ucd_list_combo = ttk.Combobox(
com_frame,
textvariable=self.ucd_list_var,
values=available_ports,
values=available_ucd_list,
width=10,
state="readonly",
)
@@ -453,6 +503,19 @@ def create_test_type_frame(self):
)
self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1)
# 测试版水印标签(版本 x.x.0.0 时显示)
from app_version import is_beta_version, APP_VERSION
if is_beta_version():
beta_lbl = tk.Label(
self.sidebar_frame,
text=f"[测试版] v{APP_VERSION}",
foreground="#ffffff",
background="#cc3300",
font=("微软雅黑", 8, "bold"),
anchor="center",
)
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
# 注册面板按钮
if hasattr(self, "panels"):
if "log" in self.panels:
@@ -569,6 +632,74 @@ def on_screen_module_timing_changed(self, event=None):
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
def on_sdr_timing_changed(self, event=None):
"""SDR测试分辨率改变时的回调"""
try:
selected_timing = self.sdr_timing_var.get()
self.log_gui.log(f"SDR测试分辨率已更改为: {selected_timing}", level="info")
# 直接更新 sdr_movie 的 timing 配置
self.config.current_test_types["sdr_movie"]["timing"] = selected_timing
if self.testing:
self.log_gui.log("警告: 测试进行中,分辨率更改将在下次测试时生效", level="error")
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"SDR测试分辨率更改失败: {str(e)}", level="error")
def on_sdr_output_format_changed(self, event=None):
"""SDR 色彩格式改变时的回调"""
try:
fmt = self.sdr_output_format_var.get()
self.log_gui.log(f"SDR色彩格式已更改为: {fmt}", level="info")
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
return
if getattr(self.ucd, "status", False):
ok = self.ucd.apply_signal_format(
color_space=self.sdr_color_space_var.get(),
data_range=self.sdr_data_range_var.get(),
bit_depth=self.sdr_bit_depth_var.get(),
color_format=fmt,
)
if not ok:
self.log_gui.log("SDR色彩格式应用到UCD失败", level="error")
except Exception as e:
self.log_gui.log(f"SDR色彩格式更改失败: {str(e)}", level="error")
def on_hdr_output_format_changed(self, event=None):
"""HDR 色彩格式改变时的回调"""
try:
fmt = self.hdr_output_format_var.get()
self.log_gui.log(f"HDR色彩格式已更改为: {fmt}", level="info")
if self.testing:
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
return
if getattr(self.ucd, "status", False):
ok = self.ucd.apply_signal_format(
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(),
color_format=fmt,
)
if not ok:
self.log_gui.log("HDR色彩格式应用到UCD失败", level="error")
except Exception as e:
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
def update_test_items(self):
"""根据当前测试类型更新测试项目复选框"""
# 先隐藏所有测试项目框架

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import datetime
import os
import sys
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
@@ -11,7 +12,7 @@ from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
_TEMPLATE_FILE = "pantone\xa02670\xa0colors.xlsx"
_TEMPLATE_FILE = "pantone_2670_colors.xlsx"
def create_pantone_baseline_panel(self):
@@ -153,8 +154,22 @@ def toggle_pantone_baseline_panel(self):
self.show_panel("pantone_baseline")
def _get_settings_dir(self):
"""返回 settings 绝对目录,避免依赖当前工作目录。"""
if getattr(self, "config_file", None):
return os.path.dirname(self.config_file)
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
return os.path.join(base_dir, "settings")
def _load_patterns(self):
path = os.path.join("settings", _TEMPLATE_FILE)
path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
if not os.path.isfile(path):
raise FileNotFoundError(f"未找到模板文件: {path}")
@@ -513,7 +528,7 @@ def _auto_save_template(self):
def _write_template_xlsx(self, path):
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
template_path = os.path.join("settings", _TEMPLATE_FILE)
template_path = os.path.join(_get_settings_dir(self), _TEMPLATE_FILE)
from openpyxl import load_workbook, Workbook
if os.path.isfile(template_path):

View File

@@ -1,6 +1,15 @@
APP_NAME = "PQ 自动化测试工具"
APP_VERSION = "0.1.0"
APP_VERSION = "106.26.0.0"
def is_beta_version(version: str = APP_VERSION) -> bool:
"""版本号第3、4段均为 '0' 时(格式 x.x.0.0)判定为测试版。"""
parts = version.split(".")
if len(parts) >= 4:
return parts[2] == "0" and parts[3] == "0"
return False
def get_app_title():
return f"{APP_NAME} v{APP_VERSION}"
suffix = " [测试版]" if is_beta_version() else ""
return f"{APP_NAME} v{APP_VERSION}{suffix}"

View File

@@ -522,11 +522,7 @@ class UCDEnum:
@staticmethod
def get_list():
return [
SignalFormat.GammaType.GAMMA_22,
SignalFormat.GammaType.GAMMA_24,
SignalFormat.GammaType.GAMMA_26,
]
return ["2.2", "2.4", "2.6"]
@staticmethod
def get_gamma_value(gamma_str):
@@ -542,12 +538,7 @@ class UCDEnum:
@staticmethod
def get_list():
return [SignalFormat.DataRange.FULL, SignalFormat.DataRange.LIMITED]
@staticmethod
def is_full_range(range_str):
"""判断是否为 Full Range"""
return range_str == SignalFormat.DataRange.FULL
return ["Full", "Limited"]
class BitDepth:
"""编码位深枚举"""
@@ -558,11 +549,7 @@ class UCDEnum:
@staticmethod
def get_list():
return [
SignalFormat.BitDepth.BIT_8,
SignalFormat.BitDepth.BIT_10,
SignalFormat.BitDepth.BIT_12,
]
return ["8bit", "10bit", "12bit"]
@staticmethod
def get_bit_value(bit_str):
@@ -583,8 +570,44 @@ class UCDEnum:
@staticmethod
def get_maxcll_list():
return SignalFormat.HDRMetadata.MAX_CLL_OPTIONS
return [400, 600, 800, 1000, 1200, 1500, 2000, 4000]
@staticmethod
def get_maxfall_list():
return SignalFormat.HDRMetadata.MAX_FALL_OPTIONS
return [200, 300, 400, 500, 600, 800, 1000]
class OutputFormat:
"""输出色彩格式枚举(决定信号是 RGB 还是 YCbCr 格式)"""
RGB = "RGB"
YCBCR_422 = "YCbCr 4:2:2"
YCBCR_444 = "YCbCr 4:4:4"
YCBCR_420 = "YCbCr 4:2:0"
Y_ONLY = "Y Only"
IDO_DEFINED = "IDO Defined"
RAW = "RAW"
DSC = "DSC"
@staticmethod
def get_list():
return ["RGB", "YCbCr 4:4:4", "YCbCr 4:2:2", "YCbCr 4:2:0",
"Y Only", "IDO Defined", "RAW", "DSC"]
@staticmethod
def is_ycbcr(format_str):
return "YCbCr" in (format_str or "")
@staticmethod
def get_format_key(format_str):
"""将显示字符串转换为 UCDEnum.ColorInfo.get_color_format() 的 key"""
fmt_map = {
"RGB": "rgb",
"YCbCr 4:4:4": "ycbcr444",
"YCbCr 4:2:2": "ycbcr422",
"YCbCr 4:2:0": "ycbcr420",
"Y Only": "yonly",
"IDO Defined": "ido_defined",
"RAW": "raw",
"DSC": "dsc",
}
return fmt_map.get(format_str, "rgb")

View File

@@ -14,7 +14,7 @@ class UCDController:
self.role = None
self.timing_manager = None
self.config = None
self.color_mode = None
self.color_info = None
self.status = False
self.current_interface = "HDMI"
@@ -51,7 +51,7 @@ class UCDController:
pg, _ = self.get_tx_modules()
self.timing_manager = pg.timing_manager
self.color_mode = UniTAP.ColorInfo()
self.color_info = UniTAP.ColorInfo()
self.status = True
return True
@@ -60,23 +60,26 @@ class UCDController:
self._force_cleanup()
return False
def _reset_state(self):
"""重置所有运行时状态(不关闭设备句柄)"""
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
def close(self):
"""关闭设备"""
try:
if self.dev:
self._close_device_object(self.dev)
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._reset_state()
self.lUniTAP = None
for i in range(3):
@@ -89,16 +92,7 @@ class UCDController:
return True
except Exception as e:
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._reset_state()
try:
self.lUniTAP = None
@@ -135,16 +129,7 @@ class UCDController:
if self.dev:
self._close_device_object(self.dev)
self.dev = None
self.role = None
self.status = False
self.timing_manager = None
self.current_timing = None
self.current_pattern = None
self.current_pattern_param = None
self.current_pattern_params = None
self.current_pattern_index = 0
self.current_interface = "HDMI"
self._reset_state()
except Exception as e:
pass
@@ -210,22 +195,12 @@ class UCDController:
pg, _ = self.get_tx_modules()
self.timing_manager = pg.timing_manager
if test_type == "hdr_movie":
color_format_str = self.config.current_test_types[test_type]["color_format"]
color_format = UCDEnum.ColorInfo.get_color_format(color_format_str)
color_format = self.config.current_test_types[test_type]["color_format"]
bpc = self.config.current_test_types[test_type]["bpc"]
colorimetry = self.config.current_test_types[test_type]["colorimetry"]
if color_format:
self.color_mode.color_format = color_format
else:
return False
else:
color_format = self.config.current_test_types[test_type]["color_format"]
bpc = self.config.current_test_types[test_type]["bpc"]
colorimetry = self.config.current_test_types[test_type]["colorimetry"]
if not self.set_color_mode(color_format, bpc, colorimetry):
return False
if not self.set_color_mode(color_format, bpc, colorimetry):
return False
timing_str = self.config.current_test_types[test_type]["timing"]
self.set_timing_from_string(timing_str)
@@ -251,7 +226,7 @@ class UCDController:
return True
def send_image_pattern(self, image_path):
"""发送图片 Pattern依赖当前 timing/color_mode 状态)。"""
"""发送图片 Pattern依赖当前 timing/color_info 状态)。"""
if not self.status or not self.role:
return False
@@ -265,7 +240,7 @@ class UCDController:
return False
def send_solid_rgb_pattern(self, rgb):
"""发送纯色 RGB Pattern依赖当前 timing/color_mode 状态)。"""
"""发送纯色 RGB Pattern依赖当前 timing/color_info 状态)。"""
if not self.status or not self.role:
return False
@@ -300,14 +275,7 @@ class UCDController:
def set_color_mode(self, cf, bpc, cm):
"""设置颜色模式"""
current_dynamic_range = None
current_transfer_characteristic = None
if hasattr(self.color_mode, "dynamic_range"):
current_dynamic_range = self.color_mode.dynamic_range
if hasattr(self.color_mode, "transfer_characteristic"):
current_transfer_characteristic = self.color_mode.transfer_characteristic
current_dynamic_range = self.color_info.dynamic_range
color_format = UCDEnum.ColorInfo.get_color_format(cf)
if color_format is None:
@@ -320,20 +288,15 @@ class UCDController:
if colorimetry is None:
return False
self.color_mode.color_format = color_format
self.color_mode.bpc = bpc
self.color_mode.colorimetry = colorimetry
if current_dynamic_range is not None:
self.color_mode.dynamic_range = current_dynamic_range
if current_transfer_characteristic is not None:
self.color_mode.transfer_characteristic = current_transfer_characteristic
self.color_info.color_format = color_format
self.color_info.bpc = bpc
self.color_info.colorimetry = colorimetry
self.color_info.dynamic_range = current_dynamic_range
return True
def apply_video_mode(self):
"""应用当前colormode和timing"""
"""应用当前 color_info 和 timing"""
if self.current_timing:
self.set_video_mode()
return True
@@ -341,25 +304,34 @@ class UCDController:
def set_video_mode(self):
"""设置视频模式"""
# 对比上次发出的配置,判断是否会触发电视重新锁定信号
current_config = (
self.current_timing,
self.color_info.color_format,
self.color_info.colorimetry,
self.color_info.dynamic_range,
self.color_info.bpc,
)
self.format_changed = (current_config != getattr(self, "_last_sent_config", None))
video_mode = UniTAP.VideoMode(
timing=self.current_timing, color_info=self.color_mode
timing=self.current_timing, color_info=self.color_info
)
pg, _ = self.get_tx_modules()
pg.set_vm(vm=video_mode)
self._last_sent_config = current_config
return True
def set_pattern(self, pattern, pattern_params=None):
"""设置pattern"""
if self.current_timing:
if (
pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips
or pattern
== UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern
or pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow
and pattern_params
):
needs_params = {
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips,
UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes,
UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern,
UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow,
}
if pattern in needs_params and pattern_params:
self.set_pattern_params(pattern, pattern_params)
return True
return False
@@ -552,138 +524,62 @@ class UCDController:
else:
return False
def set_sdr_format(
self, color_space=None, gamma=None, data_range=None, bit_depth=None
def apply_signal_format(
self, color_space=None, data_range=None, bit_depth=None, color_format=None, **_
):
"""设置SDR信号格式"""
def _get_colorimetry_from_color_space(color_space_name):
"""将色彩空间名称转换为UniTAP colorimetry枚举值"""
try:
colorimetry_map = {
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
}
result = colorimetry_map.get(color_space_name)
return result if result else UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
except Exception as e:
return UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709
def _set_gamma_transfer_characteristic(gamma_value_str):
"""设置Gamma传输特性"""
try:
gamma_value = float(gamma_value_str)
if abs(gamma_value - 2.2) < 0.1:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return True
elif abs(gamma_value - 2.4) < 0.1:
if hasattr(
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA24"
):
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA24
)
return True
else:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return False
elif abs(gamma_value - 2.6) < 0.1:
if hasattr(
UniTAP.ColorInfo.TransferCharacteristic, "TRANSFER_GAMMA26"
):
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_GAMMA26
)
return True
else:
self.color_mode.transfer_characteristic = (
UniTAP.ColorInfo.TransferCharacteristic.TRANSFER_BT709
)
return False
else:
return False
except Exception as e:
return False
"""统一设置信号格式color_format / colorimetry / dynamic_range / bpc
Gamma/EOTF 传输特性在 ColorInfo API 中不存在;
max_cll / max_fall 暂无对应 SDK 接口,通过 **_ 接收后忽略。
"""
try:
if color_space:
colorimetry = _get_colorimetry_from_color_space(color_space)
if colorimetry:
self.color_mode.colorimetry = colorimetry
if color_format:
fmt_key = UCDEnum.SignalFormat.OutputFormat.get_format_key(color_format)
cf = UCDEnum.ColorInfo.get_color_format(fmt_key)
if cf is not None:
self.color_info.color_format = cf
if gamma:
_set_gamma_transfer_characteristic(gamma)
if color_space:
colorimetry = self._get_colorimetry_from_color_space(color_space, color_format)
if colorimetry:
self.color_info.colorimetry = colorimetry
if data_range:
if data_range == "Full":
self.color_mode.dynamic_range = (
UniTAP.ColorInfo.DynamicRange.DR_VESA
)
self.color_info.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
elif data_range == "Limited":
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
self.color_info.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
if bit_depth:
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
self.color_mode.bpc = bpc
self.color_info.bpc = bpc
if self.current_timing:
self.set_video_mode()
return True
except Exception as e:
except Exception:
return False
def set_hdr_format(
self,
color_space=None,
data_range=None,
bit_depth=None,
max_cll=None,
max_fall=None,
):
"""设置HDR信号格式"""
try:
if color_space:
colorimetry = self._get_colorimetry_from_color_space(color_space)
if colorimetry:
self.color_mode.colorimetry = colorimetry
# 向后兼容别名
set_sdr_format = apply_signal_format
set_hdr_format = apply_signal_format
if data_range:
if data_range == "Full":
self.color_mode.dynamic_range = (
UniTAP.ColorInfo.DynamicRange.DR_VESA
)
elif data_range == "Limited":
self.color_mode.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
if bit_depth:
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
self.color_mode.bpc = bpc
if self.current_timing:
self.set_video_mode()
return True
except Exception as e:
return False
def _get_colorimetry_from_color_space(self, color_space):
"""将色彩空间字符串转换为UniTAP.ColorInfo.Colorimetry"""
def _get_colorimetry_from_color_space(self, color_space, color_format=None):
"""将色彩空间字符串转换为UniTAP.ColorInfo.Colorimetry。
BT.2020 在 YCbCr 输出时使用 CM_ITUR_BT2020_YCbCrRGB 输出时使用 CM_ITUR_BT2020_RGB。
"""
is_ycbcr = UCDEnum.SignalFormat.OutputFormat.is_ycbcr(color_format)
bt2020_cm = (
UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr
if is_ycbcr
else UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
)
colorimetry_map = {
"sRGB": UniTAP.ColorInfo.Colorimetry.CM_sRGB,
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
"BT.2020": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB,
"BT.2020": bt2020_cm,
"DCI-P3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
}
return colorimetry_map.get(color_space)

View File

@@ -57,10 +57,14 @@ Section "Main Installation" SEC_MAIN
SetOutPath "$INSTDIR\internal"
File /r /x "*.pdb" /x "*.lib" /x "*.exp" /x "*.h" /x "__pycache__" /x "*.pyc" "${DIST_ROOT}\internal\*.*"
IfFileExists "$INSTDIR\settings\pq_config.json" +3 0
SetOutPath "$INSTDIR\settings"
SetOutPath "$INSTDIR\settings"
IfFileExists "$INSTDIR\settings\pq_config.json" +2 0
File /oname=pq_config.json "${PROJECT_ROOT}\settings\pq_config.json"
IfFileExists "$INSTDIR\settings\pantone_2670_colors.xlsx" +2 0
File /oname=pantone_2670_colors.xlsx "${PROJECT_ROOT}\settings\pantone_2670_colors.xlsx"
WriteUninstaller "$INSTDIR\Uninstall.exe"
WriteRegStr HKCU "${APP_REG_KEY}" "InstallDir" "$INSTDIR"

View File

@@ -289,8 +289,11 @@ class PQAutomationApp:
create_test_type_frame = _main.create_test_type_frame
update_config_info_display = _main.update_config_info_display
on_screen_module_timing_changed = _main.on_screen_module_timing_changed
on_sdr_timing_changed = _main.on_sdr_timing_changed
update_test_items = _main.update_test_items
on_test_type_change = _main.on_test_type_change
on_sdr_output_format_changed = _main.on_sdr_output_format_changed
on_hdr_output_format_changed = _main.on_hdr_output_format_changed
create_cct_params_frame = _ccp.create_cct_params_frame
on_sdr_cct_param_focus_out = _ccp.on_sdr_cct_param_focus_out

View File

@@ -3,6 +3,12 @@
import os
import sys
from PyInstaller.utils.hooks import collect_dynamic_libs, collect_data_files
numpy_binaries = collect_dynamic_libs('numpy')
matplotlib_datas = collect_data_files('matplotlib')
SPEC_DIR = (
os.path.abspath(os.path.dirname(__file__))
if "__file__" in globals()
@@ -22,20 +28,10 @@ from PyInstaller.utils.win32.versioninfo import (
VarStruct,
)
def build_windows_version(version_text):
parts = [int(part) for part in version_text.split('.') if part.strip()]
parts = (parts + [0, 0, 0, 0])[:4]
return tuple(parts)
windows_version = build_windows_version(APP_VERSION)
windows_version_text = '.'.join(str(part) for part in windows_version)
version_info = VSVersionInfo(
ffi=FixedFileInfo(
filevers=windows_version,
prodvers=windows_version,
filevers=(5, 26, 1519, 2),
prodvers=(5, 0, 1, 0),
mask=0x3F,
flags=0x0,
OS=0x40004,
@@ -51,11 +47,11 @@ version_info = VSVersionInfo(
[
StringStruct('CompanyName', 'Moka'),
StringStruct('FileDescription', APP_NAME),
StringStruct('FileVersion', windows_version_text),
StringStruct('FileVersion', '5.26.1519.2'),
StringStruct('InternalName', 'pqAutomationApp'),
StringStruct('OriginalFilename', 'pqAutomationApp.exe'),
StringStruct('ProductName', APP_NAME),
StringStruct('ProductVersion', windows_version_text),
StringStruct('ProductVersion', '5.0.1.0'),
],
)
]
@@ -68,8 +64,8 @@ version_info = VSVersionInfo(
a = Analysis(
['pqAutomationApp.py'],
pathex=[],
binaries=[],
datas=[('assets', 'assets'), ('UniTAP', 'UniTAP')],
binaries=numpy_binaries,
datas=[('assets', 'assets'), ('UniTAP', 'UniTAP')] + matplotlib_datas,
hiddenimports=[
# app 包的子模块在主文件里是静态 import 的,但惰性调用 / 属性绑定
# 场景较多,显式列出可避免 PyInstaller 漏打包。

View File

@@ -9,7 +9,7 @@
"cct",
"contrast"
],
"timing": "DMT 1920x 1080 @ 60Hz",
"timing": "DMT 1600x 1200 @ 60Hz",
"color_format": "RGB",
"bpc": 8,
"colorimetry": "sRGB",
@@ -24,11 +24,7 @@
"sdr_movie": {
"name": "SDR Movie测试",
"test_items": [
"gamut",
"gamma",
"cct",
"contrast",
"accuracy"
"gamut"
],
"timing": "DMT 1920x 1080 @ 60Hz",
"color_format": "RGB",
@@ -40,7 +36,7 @@
"y_ideal": 0.329,
"y_tolerance": 0.003
},
"gamut_reference": "DCI-P3"
"gamut_reference": "BT.2020"
},
"hdr_movie": {
"name": "HDR Movie测试",
@@ -64,7 +60,7 @@
}
},
"device_config": {
"ca_com": "COM6",
"ca_com": "COM3",
"ucd_list": "0: UCD-323 [2128C209]",
"ca_channel": "0"
},