修改AI生图接口、修改设备连接UI、修改LocalDimming逻辑和UI
This commit is contained in:
@@ -74,12 +74,19 @@ _DEFAULT_CCT_PARAMS = {
|
|||||||
"y_ideal": 0.3290,
|
"y_ideal": 0.3290,
|
||||||
"y_tolerance": 0.003,
|
"y_tolerance": 0.003,
|
||||||
},
|
},
|
||||||
|
"local_dimming": {
|
||||||
|
"x_ideal": 0.3127,
|
||||||
|
"x_tolerance": 0.003,
|
||||||
|
"y_ideal": 0.3290,
|
||||||
|
"y_tolerance": 0.003,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT_GAMUT_REFERENCE = {
|
_DEFAULT_GAMUT_REFERENCE = {
|
||||||
"screen_module": "DCI-P3",
|
"screen_module": "DCI-P3",
|
||||||
"sdr_movie": "BT.709",
|
"sdr_movie": "BT.709",
|
||||||
"hdr_movie": "BT.2020",
|
"hdr_movie": "BT.2020",
|
||||||
|
"local_dimming": "DCI-P3",
|
||||||
}
|
}
|
||||||
|
|
||||||
_DEFAULT_TEST_TYPES = {
|
_DEFAULT_TEST_TYPES = {
|
||||||
@@ -113,6 +120,16 @@ _DEFAULT_TEST_TYPES = {
|
|||||||
"colorimetry": "sRGB",
|
"colorimetry": "sRGB",
|
||||||
"patterns": {"gamut": "rgb", "eotf": "gray", "cct": "gray", "contrast": "rgb", "accuracy": "accuracy"},
|
"patterns": {"gamut": "rgb", "eotf": "gray", "cct": "gray", "contrast": "rgb", "accuracy": "accuracy"},
|
||||||
},
|
},
|
||||||
|
"local_dimming": {
|
||||||
|
"name": "Local Dimming",
|
||||||
|
"test_items": [],
|
||||||
|
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||||
|
"data_range": "Full",
|
||||||
|
"color_format": "RGB",
|
||||||
|
"bpc": 8,
|
||||||
|
"colorimetry": "sRGB",
|
||||||
|
"patterns": {},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
_PATTERN_RGB = {
|
_PATTERN_RGB = {
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ def run_test(self: "PQAutomationApp", test_type, test_items):
|
|||||||
self.run_sdr_movie_test(test_items)
|
self.run_sdr_movie_test(test_items)
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
self.run_hdr_movie_test(test_items)
|
self.run_hdr_movie_test(test_items)
|
||||||
|
elif test_type == "local_dimming":
|
||||||
|
self.log_gui.log(
|
||||||
|
"Local Dimming 为手动模式,请在 Local Dimming 面板发送图案并采集亮度",
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
|
||||||
# 测试完成后更新UI状态
|
# 测试完成后更新UI状态
|
||||||
if self.testing: # 如果没有被中途停止
|
if self.testing: # 如果没有被中途停止
|
||||||
@@ -1073,226 +1078,6 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
|
|||||||
self.log_gui.log("色准测试完成", level="success")
|
self.log_gui.log("色准测试完成", level="success")
|
||||||
|
|
||||||
|
|
||||||
def run_simulation_test(self: "PQAutomationApp"):
|
|
||||||
"""运行模拟测试(无需 UCD/CA),直接在 UI 展示结果。"""
|
|
||||||
try:
|
|
||||||
test_type = self.config.current_test_type
|
|
||||||
selected_items = self.get_selected_test_items()
|
|
||||||
|
|
||||||
if not selected_items:
|
|
||||||
self.log_gui.log("未选择测试项目,无法执行模拟测试", level="error")
|
|
||||||
messagebox.showwarning("提示", "请先勾选至少一个测试项目")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log_gui.log("=" * 60, level="separator")
|
|
||||||
self.log_gui.log("开始执行模拟测试(无需 UCD/CA 设备)", level="info")
|
|
||||||
self.log_gui.log(f"测试类型: {self.get_test_type_name(test_type)}", level="info")
|
|
||||||
self.log_gui.log(
|
|
||||||
f"测试项目: {', '.join(self.config.get_test_item_chinese_names(selected_items))}",
|
|
||||||
level="info",
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasattr(self, "update_chart_tabs_state"):
|
|
||||||
self.update_chart_tabs_state()
|
|
||||||
if hasattr(self, "clear_chart"):
|
|
||||||
self.clear_chart()
|
|
||||||
|
|
||||||
self.new_pq_results(test_type, f"{self.get_test_type_name(test_type)} 模拟测试")
|
|
||||||
self.status_var.set("模拟测试进行中...")
|
|
||||||
|
|
||||||
rng = np.random.default_rng()
|
|
||||||
|
|
||||||
def _read_ideal_xy():
|
|
||||||
try:
|
|
||||||
if test_type == "sdr_movie":
|
|
||||||
return float(self.sdr_cct_x_ideal_var.get()), float(self.sdr_cct_y_ideal_var.get())
|
|
||||||
if test_type == "hdr_movie":
|
|
||||||
return float(self.hdr_cct_x_ideal_var.get()), float(self.hdr_cct_y_ideal_var.get())
|
|
||||||
return float(self.cct_x_ideal_var.get()), float(self.cct_y_ideal_var.get())
|
|
||||||
except Exception:
|
|
||||||
return 0.3127, 0.3290
|
|
||||||
|
|
||||||
def _xyY_to_xyz_row(x, y, lv):
|
|
||||||
if y <= 1e-8:
|
|
||||||
return [x, y, lv, 0.0, lv, 0.0]
|
|
||||||
X = x * lv / y
|
|
||||||
Z = (1 - x - y) * lv / y
|
|
||||||
return [x, y, lv, X, lv, Z]
|
|
||||||
|
|
||||||
# 共享灰阶数据:用于 Gamma/EOTF/CCT/对比度
|
|
||||||
gray_results = []
|
|
||||||
x_ideal, y_ideal = _read_ideal_xy()
|
|
||||||
peak_lv = 900.0 if test_type == "hdr_movie" else 220.0
|
|
||||||
gamma_shape = 2.25 if test_type == "hdr_movie" else 2.20
|
|
||||||
|
|
||||||
for i in range(11):
|
|
||||||
p = i / 10.0
|
|
||||||
lv = 0.08 + peak_lv * (p ** gamma_shape)
|
|
||||||
lv *= 1.0 + float(rng.normal(0.0, 0.015))
|
|
||||||
lv = max(lv, 0.03)
|
|
||||||
|
|
||||||
x = x_ideal + float(rng.normal(0.0, 0.0012))
|
|
||||||
y = y_ideal + float(rng.normal(0.0, 0.0012))
|
|
||||||
gray_results.append(_xyY_to_xyz_row(x, y, lv))
|
|
||||||
|
|
||||||
if any(item in selected_items for item in ("gamma", "eotf", "cct", "contrast")):
|
|
||||||
self.results.add_intermediate_data("shared", "gray", gray_results)
|
|
||||||
|
|
||||||
# 色域模拟
|
|
||||||
if "gamut" in selected_items:
|
|
||||||
ref_map = {
|
|
||||||
"BT.709": [(0.6400, 0.3300), (0.3000, 0.6000), (0.1500, 0.0600)],
|
|
||||||
"DCI-P3": [(0.6800, 0.3200), (0.2650, 0.6900), (0.1500, 0.0600)],
|
|
||||||
"BT.2020": [(0.7080, 0.2920), (0.1700, 0.7970), (0.1310, 0.0460)],
|
|
||||||
"BT.601": [(0.6300, 0.3400), (0.3100, 0.5950), (0.1550, 0.0700)],
|
|
||||||
}
|
|
||||||
|
|
||||||
if test_type == "hdr_movie":
|
|
||||||
reference = self.hdr_gamut_ref_var.get() if hasattr(self, "hdr_gamut_ref_var") else "BT.2020"
|
|
||||||
elif test_type == "sdr_movie":
|
|
||||||
reference = self.sdr_gamut_ref_var.get() if hasattr(self, "sdr_gamut_ref_var") else "BT.709"
|
|
||||||
else:
|
|
||||||
reference = self.screen_gamut_ref_var.get() if hasattr(self, "screen_gamut_ref_var") else "DCI-P3"
|
|
||||||
|
|
||||||
if reference not in ref_map:
|
|
||||||
reference = "DCI-P3"
|
|
||||||
|
|
||||||
gamut_results = []
|
|
||||||
for rx, ry in ref_map[reference]:
|
|
||||||
mx = rx + float(rng.normal(0.0, 0.006))
|
|
||||||
my = ry + float(rng.normal(0.0, 0.006))
|
|
||||||
gamut_results.append(_xyY_to_xyz_row(mx, my, 120.0))
|
|
||||||
|
|
||||||
self.results.add_intermediate_data("gamut", "rgb", gamut_results)
|
|
||||||
self.results.set_test_item_result(
|
|
||||||
"gamut",
|
|
||||||
{
|
|
||||||
"area": 0.0,
|
|
||||||
"coverage": 95.0,
|
|
||||||
"uv_coverage": 93.0,
|
|
||||||
"reference": reference,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.plot_gamut(gamut_results, 95.0, test_type)
|
|
||||||
|
|
||||||
# Gamma / EOTF 模拟
|
|
||||||
if "gamma" in selected_items and test_type != "hdr_movie":
|
|
||||||
pattern_params = self.config.default_pattern_gray.get("pattern_params", None)
|
|
||||||
results_with_gamma, L_bar = self.calculate_gamma(
|
|
||||||
gray_results, len(gray_results) - 1, pattern_params
|
|
||||||
)
|
|
||||||
self.results.set_test_item_result("gamma", {"gamma": results_with_gamma, "L_bar": L_bar})
|
|
||||||
try:
|
|
||||||
target_gamma = float(self.sdr_gamma_type_var.get()) if test_type == "sdr_movie" else 2.2
|
|
||||||
except Exception:
|
|
||||||
target_gamma = 2.2
|
|
||||||
self.plot_gamma(L_bar, results_with_gamma, target_gamma, test_type)
|
|
||||||
|
|
||||||
if "eotf" in selected_items and test_type == "hdr_movie":
|
|
||||||
pattern_params = self.config.default_pattern_gray.get("pattern_params", None)
|
|
||||||
results_with_eotf, L_bar = self.calculate_gamma(
|
|
||||||
gray_results, len(gray_results) - 1, pattern_params
|
|
||||||
)
|
|
||||||
self.results.set_test_item_result("eotf", {"eotf": results_with_eotf, "L_bar": L_bar})
|
|
||||||
self.plot_eotf(L_bar, results_with_eotf, test_type)
|
|
||||||
|
|
||||||
# CCT 模拟
|
|
||||||
if "cct" in selected_items:
|
|
||||||
cct_values = pq_algorithm.calculate_cct_from_results(gray_results)
|
|
||||||
self.results.set_test_item_result("cct", {"cct_values": cct_values})
|
|
||||||
self.plot_cct(test_type)
|
|
||||||
|
|
||||||
# 对比度模拟
|
|
||||||
if "contrast" in selected_items:
|
|
||||||
luminance_values = [row[2] for row in gray_results]
|
|
||||||
max_luminance = max(luminance_values)
|
|
||||||
min_luminance = max(min(luminance_values), 0.001)
|
|
||||||
contrast_ratio = max_luminance / min_luminance
|
|
||||||
contrast_data = {
|
|
||||||
"max_luminance": max_luminance,
|
|
||||||
"min_luminance": min_luminance,
|
|
||||||
"contrast_ratio": contrast_ratio,
|
|
||||||
"luminance_values": luminance_values,
|
|
||||||
}
|
|
||||||
self.results.set_test_item_result("contrast", contrast_data)
|
|
||||||
self.plot_contrast(contrast_data, test_type)
|
|
||||||
|
|
||||||
# 色准模拟
|
|
||||||
if "accuracy" in selected_items:
|
|
||||||
color_names = self.config.get_accuracy_color_names()
|
|
||||||
standards = self.get_accuracy_color_standards(test_type)
|
|
||||||
|
|
||||||
color_patches = []
|
|
||||||
measured_data = []
|
|
||||||
delta_e_values = []
|
|
||||||
|
|
||||||
for idx, name in enumerate(color_names):
|
|
||||||
sx, sy = standards.get(name, (0.3127, 0.3290))
|
|
||||||
|
|
||||||
# 前 20 个色块偏差更小,后 9 个稍大,方便 UI 看出差异
|
|
||||||
noise_sigma = 0.0008 if idx < 20 else 0.0018
|
|
||||||
mx = sx + float(rng.normal(0.0, noise_sigma))
|
|
||||||
my = sy + float(rng.normal(0.0, noise_sigma))
|
|
||||||
lv = max(5.0, 40.0 + idx * 2.3 + float(rng.normal(0.0, 4.0)))
|
|
||||||
|
|
||||||
row = _xyY_to_xyz_row(mx, my, lv)
|
|
||||||
measured_data.append(row)
|
|
||||||
color_patches.append(name)
|
|
||||||
|
|
||||||
delta_e = self.calculate_delta_e_2000(mx, my, lv, sx, sy)
|
|
||||||
delta_e_values.append(delta_e)
|
|
||||||
|
|
||||||
avg_delta_e = float(np.mean(delta_e_values)) if delta_e_values else 0.0
|
|
||||||
max_delta_e = float(np.max(delta_e_values)) if delta_e_values else 0.0
|
|
||||||
min_delta_e = float(np.min(delta_e_values)) if delta_e_values else 0.0
|
|
||||||
|
|
||||||
excellent_count = sum(1 for d in delta_e_values if d < 3)
|
|
||||||
good_count = sum(1 for d in delta_e_values if 3 <= d < 5)
|
|
||||||
poor_count = sum(1 for d in delta_e_values if d >= 5)
|
|
||||||
|
|
||||||
delta_e_gray = delta_e_values[0:5]
|
|
||||||
delta_e_colorchecker = delta_e_values[5:23]
|
|
||||||
delta_e_saturated = delta_e_values[23:29]
|
|
||||||
|
|
||||||
try:
|
|
||||||
target_gamma = float(self.sdr_gamma_type_var.get()) if test_type == "sdr_movie" else 2.2
|
|
||||||
except Exception:
|
|
||||||
target_gamma = 2.2
|
|
||||||
|
|
||||||
accuracy_data = {
|
|
||||||
"color_patches": color_patches,
|
|
||||||
"delta_e_values": delta_e_values,
|
|
||||||
"color_measurements": measured_data,
|
|
||||||
"avg_delta_e": avg_delta_e,
|
|
||||||
"max_delta_e": max_delta_e,
|
|
||||||
"min_delta_e": min_delta_e,
|
|
||||||
"excellent_count": excellent_count,
|
|
||||||
"good_count": good_count,
|
|
||||||
"poor_count": poor_count,
|
|
||||||
"avg_delta_e_gray": float(np.mean(delta_e_gray)) if delta_e_gray else 0.0,
|
|
||||||
"avg_delta_e_colorchecker": float(np.mean(delta_e_colorchecker)) if delta_e_colorchecker else 0.0,
|
|
||||||
"avg_delta_e_saturated": float(np.mean(delta_e_saturated)) if delta_e_saturated else 0.0,
|
|
||||||
"target_gamma": target_gamma,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.results.add_intermediate_data("accuracy", "measured", measured_data)
|
|
||||||
self.results.set_test_item_result("accuracy", accuracy_data)
|
|
||||||
self.plot_accuracy(accuracy_data, test_type)
|
|
||||||
|
|
||||||
self.save_btn.config(state=tk.NORMAL)
|
|
||||||
self.status_var.set("模拟测试完成")
|
|
||||||
self.log_gui.log("模拟测试完成,结果已显示到 UI", level="success")
|
|
||||||
self.log_gui.log("=" * 60, level="separator")
|
|
||||||
messagebox.showinfo("完成", "模拟测试已完成(无需 UCD/CA)")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set("模拟测试失败")
|
|
||||||
self.log_gui.log(f"模拟测试失败: {str(e)}", level="error")
|
|
||||||
import traceback
|
|
||||||
self.log_gui.log(traceback.format_exc(), level="error")
|
|
||||||
messagebox.showerror("错误", f"模拟测试失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def on_test_completed(self: "PQAutomationApp"):
|
def on_test_completed(self: "PQAutomationApp"):
|
||||||
"""测试完成后的UI更新"""
|
"""测试完成后的UI更新"""
|
||||||
self.testing = False
|
self.testing = False
|
||||||
@@ -1530,7 +1315,6 @@ class TestRunnerMixin:
|
|||||||
test_cct = test_cct
|
test_cct = test_cct
|
||||||
test_contrast = test_contrast
|
test_contrast = test_contrast
|
||||||
test_color_accuracy = test_color_accuracy
|
test_color_accuracy = test_color_accuracy
|
||||||
run_simulation_test = run_simulation_test
|
|
||||||
on_test_completed = on_test_completed
|
on_test_completed = on_test_completed
|
||||||
on_custom_template_test_completed = on_custom_template_test_completed
|
on_custom_template_test_completed = on_custom_template_test_completed
|
||||||
get_current_test_result = get_current_test_result
|
get_current_test_result = get_current_test_result
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
"""AI 图片生成服务:后端请求 + 本地缓存管理。
|
"""AI 图片生成服务:后端请求 + 本地缓存管理。
|
||||||
|
|
||||||
后端接口(测试环境):
|
后端接口(生产/测试环境):
|
||||||
POST {API_BASE_URL}{API_PATH}
|
POST {API_BASE_URL}{API_GENERATE_PATH}
|
||||||
body: {"user_message": str, "session_id": str}
|
body: {"user_message": str, "session_id": str, "upload_image_url"?: str}
|
||||||
resp: {"code": 200, "message": "", "data": {"imageUrl": "..."}}
|
resp: {"code": 200, "message": "", "data": {"imageUrl": "..."}}
|
||||||
|
POST {API_BASE_URL}{API_UPLOAD_PATH} # multipart/form-data, field: file
|
||||||
|
resp: {"code": 200, "message": "", "data": {"upload_image_url": "..."}}
|
||||||
|
|
||||||
|
带 ``upload_image_url`` 启用"图生图"模式;多轮对话需将上一轮返回的 imageUrl
|
||||||
|
作为下一轮请求的 upload_image_url(由 panel 通过会话级缓存自动维护)。
|
||||||
|
|
||||||
缓存目录:``settings/ai_image_cache/``,每张图片有同名的 ``.json`` 侧车记录。
|
缓存目录:``settings/ai_image_cache/``,每张图片有同名的 ``.json`` 侧车记录。
|
||||||
"""
|
"""
|
||||||
@@ -41,10 +46,20 @@ _META_SUFFIX = ".json"
|
|||||||
_SUPPORTED_IMG_EXT = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
_SUPPORTED_IMG_EXT = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||||
|
|
||||||
# 测试环境后端
|
# 测试环境后端
|
||||||
# API_BASE_URL = "http://10.201.44.70:9018/ai-agent/"
|
# API_BASE_URL = "http://10.201.44.70:9008/ai-agent/"
|
||||||
API_BASE_URL = "https://rd-mokadisplay.tcl.com/ai-agent/"
|
API_BASE_URL = "https://rd-mokadisplay.tcl.com/ai-agent/"
|
||||||
API_PATH = "api/v1/pqtest/generate"
|
API_GENERATE_PATH = "api/v1/pqtest/generate"
|
||||||
API_TIMEOUT = 300.0 # 后端最长 60s,留余量
|
API_UPLOAD_PATH = "api/v1/pqtest/upload"
|
||||||
|
API_TIMEOUT = 300.0 # 后端最长 120s,留余量
|
||||||
|
API_UPLOAD_TIMEOUT = 60.0
|
||||||
|
|
||||||
|
# 上传接口限制(来自接口文档)
|
||||||
|
UPLOAD_MAX_BYTES = 10 * 1024 * 1024
|
||||||
|
UPLOAD_MAX_PIXELS = 4096
|
||||||
|
UPLOAD_ALLOWED_EXT = (".png", ".jpg", ".jpeg")
|
||||||
|
|
||||||
|
# 兼容旧名(如其他模块仍引用)
|
||||||
|
API_PATH = API_GENERATE_PATH
|
||||||
|
|
||||||
# 进程级会话 id(多轮对话需保持一致),可通过 ``reset_session`` 重置
|
# 进程级会话 id(多轮对话需保持一致),可通过 ``reset_session`` 重置
|
||||||
_session_id: str = str(uuid.uuid4())
|
_session_id: str = str(uuid.uuid4())
|
||||||
@@ -133,9 +148,9 @@ class AIImageRecord:
|
|||||||
# ---------- 后端 API ----------
|
# ---------- 后端 API ----------
|
||||||
|
|
||||||
|
|
||||||
def _api_endpoint() -> str:
|
def _api_endpoint(path: str = API_GENERATE_PATH) -> str:
|
||||||
base = API_BASE_URL if API_BASE_URL.endswith("/") else API_BASE_URL + "/"
|
base = API_BASE_URL if API_BASE_URL.endswith("/") else API_BASE_URL + "/"
|
||||||
return base + API_PATH.lstrip("/")
|
return base + path.lstrip("/")
|
||||||
|
|
||||||
|
|
||||||
def _pretty_json_text(value) -> str:
|
def _pretty_json_text(value) -> str:
|
||||||
@@ -150,22 +165,33 @@ def _pretty_json_text(value) -> str:
|
|||||||
return "" if value is None else str(value)
|
return "" if value is None else str(value)
|
||||||
|
|
||||||
|
|
||||||
def _call_pqtest_generate(user_message: str, session_id: str, timeout: float = API_TIMEOUT) -> str:
|
def _call_pqtest_generate(
|
||||||
"""调用后端 ``api/v1/pqtest/generate``,返回 imageUrl。失败抛异常。"""
|
user_message: str,
|
||||||
payload = json.dumps(
|
session_id: str,
|
||||||
{"user_message": user_message,
|
upload_image_url: Optional[str] = None,
|
||||||
"session_id": session_id},
|
timeout: float = API_TIMEOUT,
|
||||||
ensure_ascii=False,
|
) -> str:
|
||||||
).encode("utf-8")
|
"""调用后端 ``api/v1/pqtest/generate``,返回 imageUrl。失败抛异常。
|
||||||
|
|
||||||
|
``upload_image_url`` 传入时启用"图生图"模式。
|
||||||
|
"""
|
||||||
|
body: dict = {"user_message": user_message, "session_id": session_id}
|
||||||
|
if upload_image_url:
|
||||||
|
body["upload_image_url"] = upload_image_url
|
||||||
|
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||||
request_headers = {
|
request_headers = {
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"User-Agent": "pqAutomationApp/1.0",
|
"User-Agent": "pqAutomationApp/1.0",
|
||||||
}
|
}
|
||||||
endpoint = _api_endpoint()
|
endpoint = _api_endpoint(API_GENERATE_PATH)
|
||||||
logger.info(
|
logger.info(
|
||||||
"[AIImage] 请求生成 sid=%s prompt_len=%d prompt=%r",
|
"[AIImage] 请求生成 sid=%s mode=%s prompt_len=%d prompt=%r ref=%s",
|
||||||
_mask_sid(session_id), len(user_message or ""), _truncate(user_message),
|
_mask_sid(session_id),
|
||||||
|
"img2img" if upload_image_url else "txt2img",
|
||||||
|
len(user_message or ""),
|
||||||
|
_truncate(user_message),
|
||||||
|
upload_image_url or "-",
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"[AIImage][REQUEST]\nendpoint=%s\nmethod=POST\ntimeout=%.1fs\nheaders=%s\nbody=%s",
|
"[AIImage][REQUEST]\nendpoint=%s\nmethod=POST\ntimeout=%.1fs\nheaders=%s\nbody=%s",
|
||||||
@@ -250,6 +276,137 @@ def _call_pqtest_generate(user_message: str, session_id: str, timeout: float = A
|
|||||||
return image_url
|
return image_url
|
||||||
|
|
||||||
|
|
||||||
|
def _call_pqtest_upload(file_path: str, timeout: float = API_UPLOAD_TIMEOUT, auto_resize: bool = True) -> str:
|
||||||
|
"""以 multipart/form-data 上传本地图片,返回 ``upload_image_url``。失败抛异常。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
auto_resize: True 时,若图片超过 4096×4096 或 10MB 则自动缩放/重压
|
||||||
|
"""
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
raise FileNotFoundError(f"图片文件不存在: {file_path}")
|
||||||
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
|
if ext not in UPLOAD_ALLOWED_EXT:
|
||||||
|
raise ValueError(f"不支持的图片格式 ({ext}),仅支持 PNG/JPG/JPEG")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Image.open(file_path) as img:
|
||||||
|
iw, ih = img.size
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError(f"无法读取图片: {exc}") from exc
|
||||||
|
|
||||||
|
# 检查大小,如需则缩放
|
||||||
|
size = os.path.getsize(file_path)
|
||||||
|
needs_resize = (iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS or size > UPLOAD_MAX_BYTES)
|
||||||
|
|
||||||
|
if needs_resize:
|
||||||
|
if not auto_resize:
|
||||||
|
if iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS:
|
||||||
|
raise ValueError(f"分辨率超过 4096×4096(当前 {iw}×{ih})")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"图片超过 10MB 限制(当前 {size/1024/1024:.2f}MB)")
|
||||||
|
|
||||||
|
# 自动缩放:等比例缩放至 4096×4096 以内
|
||||||
|
logger.info("[AIImage][UPLOAD] 自动缩放 %dx%d (%.1fMB) 至 ≤4096×4096",
|
||||||
|
iw, ih, size/1024/1024)
|
||||||
|
scale = min(UPLOAD_MAX_PIXELS / iw, UPLOAD_MAX_PIXELS / ih, 1.0)
|
||||||
|
new_w, new_h = max(1, int(iw * scale)), max(1, int(ih * scale))
|
||||||
|
|
||||||
|
with Image.open(file_path) as img:
|
||||||
|
img_resized = img.resize((new_w, new_h), Image.LANCZOS)
|
||||||
|
# 重压至 10MB 以下
|
||||||
|
# 首先尝试原格式
|
||||||
|
tmp_io = BytesIO()
|
||||||
|
fmt = "PNG" if ext == ".png" else "JPEG"
|
||||||
|
save_kw = {"format": fmt}
|
||||||
|
img_resized.save(tmp_io, **save_kw)
|
||||||
|
tmp_bytes = tmp_io.getvalue()
|
||||||
|
|
||||||
|
if len(tmp_bytes) <= UPLOAD_MAX_BYTES:
|
||||||
|
file_bytes = tmp_bytes
|
||||||
|
else:
|
||||||
|
# 原格式太大,转换为 JPEG 并压缩
|
||||||
|
logger.info("[AIImage][UPLOAD] 原格式超过限制,转为 JPEG 并压缩")
|
||||||
|
quality = 95
|
||||||
|
while quality >= 50:
|
||||||
|
tmp_io = BytesIO()
|
||||||
|
img_resized.save(tmp_io, format="JPEG", quality=quality, optimize=True)
|
||||||
|
tmp_bytes = tmp_io.getvalue()
|
||||||
|
if len(tmp_bytes) <= UPLOAD_MAX_BYTES:
|
||||||
|
file_bytes = tmp_bytes
|
||||||
|
break
|
||||||
|
quality -= 5
|
||||||
|
else:
|
||||||
|
# 即使 quality=50 还是太大,也用这个版本上传(后端会处理)
|
||||||
|
file_bytes = tmp_bytes
|
||||||
|
|
||||||
|
logger.info("[AIImage][UPLOAD] 缩放完成 %dx%d (%.1fMB)",
|
||||||
|
new_w, new_h, len(file_bytes)/1024/1024)
|
||||||
|
iw, ih = new_w, new_h
|
||||||
|
else:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
file_bytes = f.read()
|
||||||
|
|
||||||
|
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
||||||
|
boundary = "----pqAuto" + uuid.uuid4().hex
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
crlf = b"\r\n"
|
||||||
|
body = b"".join([
|
||||||
|
b"--", boundary.encode("ascii"), crlf,
|
||||||
|
b'Content-Disposition: form-data; name="file"; filename="',
|
||||||
|
filename.encode("utf-8"), b'"', crlf,
|
||||||
|
b"Content-Type: ", mime.encode("ascii"), crlf, crlf,
|
||||||
|
file_bytes, crlf,
|
||||||
|
b"--", boundary.encode("ascii"), b"--", crlf,
|
||||||
|
])
|
||||||
|
|
||||||
|
endpoint = _api_endpoint(API_UPLOAD_PATH)
|
||||||
|
headers = {
|
||||||
|
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
"User-Agent": "pqAutomationApp/1.0",
|
||||||
|
"Content-Length": str(len(body)),
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
"[AIImage][UPLOAD] file=%s size=%dB mime=%s wh=%dx%d -> %s",
|
||||||
|
filename, len(file_bytes), mime, iw, ih, endpoint,
|
||||||
|
)
|
||||||
|
request = Request(endpoint, data=body, method="POST", headers=headers)
|
||||||
|
t0 = time.monotonic()
|
||||||
|
try:
|
||||||
|
with urlopen(request, timeout=timeout) as response:
|
||||||
|
raw = response.read()
|
||||||
|
http_status = response.status
|
||||||
|
raw_text = raw.decode("utf-8", errors="replace")
|
||||||
|
logger.info("[AIImage][UPLOAD_RESP]\nstatus=%s\nbody=%s",
|
||||||
|
http_status, _pretty_json_text(raw_text))
|
||||||
|
except HTTPError as exc:
|
||||||
|
err_raw = b""
|
||||||
|
try:
|
||||||
|
err_raw = exc.read() or b""
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
err_text = err_raw.decode("utf-8", errors="replace") if err_raw else ""
|
||||||
|
logger.error("[AIImage][UPLOAD_ERR] status=%s reason=%s body=%s",
|
||||||
|
getattr(exc, "code", "?"), str(exc), _pretty_json_text(err_text))
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("[AIImage][UPLOAD_ERR] %s: %s", type(exc).__name__, exc)
|
||||||
|
raise
|
||||||
|
elapsed = time.monotonic() - t0
|
||||||
|
try:
|
||||||
|
result = json.loads(raw.decode("utf-8"))
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError(f"上传接口返回非 JSON:{raw_text}") from exc
|
||||||
|
code = result.get("code")
|
||||||
|
message = result.get("message") or ""
|
||||||
|
data = result.get("data") or {}
|
||||||
|
url = (data.get("upload_image_url") or "").strip()
|
||||||
|
if code != 200 or not url:
|
||||||
|
raise RuntimeError(f"上传失败 code={code} msg={message or '未知错误'}")
|
||||||
|
logger.info("[AIImage][UPLOAD_OK] elapsed=%.2fs url=%s", elapsed, url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
# ---------- 缓存路径工具 ----------
|
# ---------- 缓存路径工具 ----------
|
||||||
|
|
||||||
|
|
||||||
@@ -504,6 +661,7 @@ def request_image_async(
|
|||||||
base_dir: Optional[str] = None,
|
base_dir: Optional[str] = None,
|
||||||
session_id: Optional[str] = None,
|
session_id: Optional[str] = None,
|
||||||
cancel_event: Optional[threading.Event] = None,
|
cancel_event: Optional[threading.Event] = None,
|
||||||
|
upload_image_url: Optional[str] = None,
|
||||||
) -> threading.Thread:
|
) -> threading.Thread:
|
||||||
"""在后台线程调用后端 API → 下载图片 → 写入缓存 → 回调。
|
"""在后台线程调用后端 API → 下载图片 → 写入缓存 → 回调。
|
||||||
|
|
||||||
@@ -511,24 +669,33 @@ def request_image_async(
|
|||||||
切回主线程,请在回调内部自行用 ``root.after(0, ...)``。
|
切回主线程,请在回调内部自行用 ``root.after(0, ...)``。
|
||||||
|
|
||||||
``session_id`` 留空则使用进程级会话 id(保证多轮对话上下文)。
|
``session_id`` 留空则使用进程级会话 id(保证多轮对话上下文)。
|
||||||
|
``upload_image_url`` 传入后启用"图生图"模式。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sid = session_id or get_session_id()
|
sid = session_id or get_session_id()
|
||||||
cancel = cancel_event
|
cancel = cancel_event
|
||||||
|
ref_url = (upload_image_url or "").strip() or None
|
||||||
|
|
||||||
def _worker():
|
def _worker():
|
||||||
try:
|
try:
|
||||||
if cancel is not None and cancel.is_set():
|
if cancel is not None and cancel.is_set():
|
||||||
logger.info("[AIImage] 任务已取消(请求前) sid=%s", _mask_sid(sid))
|
logger.info("[AIImage] 任务已取消(请求前) sid=%s", _mask_sid(sid))
|
||||||
return
|
return
|
||||||
image_url = _call_pqtest_generate(prompt, sid)
|
image_url = _call_pqtest_generate(prompt, sid, upload_image_url=ref_url)
|
||||||
if cancel is not None and cancel.is_set():
|
if cancel is not None and cancel.is_set():
|
||||||
logger.info("[AIImage] 任务已取消(生成后) sid=%s", _mask_sid(sid))
|
logger.info("[AIImage] 任务已取消(生成后) sid=%s", _mask_sid(sid))
|
||||||
return
|
return
|
||||||
|
extra = {
|
||||||
|
"source": "ai-api",
|
||||||
|
"session_id": sid,
|
||||||
|
"mode": "img2img" if ref_url else "txt2img",
|
||||||
|
}
|
||||||
|
if ref_url:
|
||||||
|
extra["upload_image_url"] = ref_url
|
||||||
record = import_image_from_url(
|
record = import_image_from_url(
|
||||||
image_url=image_url,
|
image_url=image_url,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
extra={"source": "ai-api", "session_id": sid},
|
extra=extra,
|
||||||
base_dir=base_dir,
|
base_dir=base_dir,
|
||||||
)
|
)
|
||||||
if cancel is not None and cancel.is_set():
|
if cancel is not None and cancel.is_set():
|
||||||
@@ -593,6 +760,38 @@ def import_image_from_url_async(
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def upload_image_async(
|
||||||
|
file_path: str,
|
||||||
|
on_success: Callable[[str], None],
|
||||||
|
on_error: Callable[[Exception], None],
|
||||||
|
cancel_event: Optional[threading.Event] = None,
|
||||||
|
timeout: float = API_UPLOAD_TIMEOUT,
|
||||||
|
auto_resize: bool = True,
|
||||||
|
) -> threading.Thread:
|
||||||
|
"""后台上传本地图片到后端,成功回调返回 ``upload_image_url``。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
auto_resize: True 时自动缩放超大图片
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
try:
|
||||||
|
if cancel_event is not None and cancel_event.is_set():
|
||||||
|
return
|
||||||
|
url = _call_pqtest_upload(file_path, timeout=timeout, auto_resize=auto_resize)
|
||||||
|
if cancel_event is not None and cancel_event.is_set():
|
||||||
|
return
|
||||||
|
on_success(url)
|
||||||
|
except Exception as exc:
|
||||||
|
if cancel_event is not None and cancel_event.is_set():
|
||||||
|
return
|
||||||
|
on_error(exc)
|
||||||
|
|
||||||
|
t = threading.Thread(target=_worker, daemon=True)
|
||||||
|
t.start()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
def is_remote_image_url(value: str) -> bool:
|
def is_remote_image_url(value: str) -> bool:
|
||||||
"""判断输入是否为 http/https 图片地址。"""
|
"""判断输入是否为 http/https 图片地址。"""
|
||||||
url = (value or "").strip()
|
url = (value or "").strip()
|
||||||
|
|||||||
@@ -181,6 +181,134 @@ def _send_ld_image(self: "PQAutomationApp", image_path):
|
|||||||
self.signal_service.send_image(image_path)
|
self.signal_service.send_image(image_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
|
||||||
|
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
|
||||||
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
|
cfg = self.config.current_test_types.get(test_type, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.signal_service.apply_config(self.config)
|
||||||
|
|
||||||
|
if test_type == "screen_module":
|
||||||
|
ok = self.signal_service.update_signal_format(
|
||||||
|
color_space=(
|
||||||
|
self.screen_module_color_space_var.get()
|
||||||
|
if hasattr(self, "screen_module_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
),
|
||||||
|
data_range=(
|
||||||
|
self.screen_module_data_range_var.get()
|
||||||
|
if hasattr(self, "screen_module_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
),
|
||||||
|
bit_depth=(
|
||||||
|
self.screen_module_bit_depth_var.get()
|
||||||
|
if hasattr(self, "screen_module_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
),
|
||||||
|
output_format=(
|
||||||
|
self.screen_module_output_format_var.get()
|
||||||
|
if hasattr(self, "screen_module_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif test_type == "sdr_movie":
|
||||||
|
ok = self.signal_service.update_signal_format(
|
||||||
|
color_space=(
|
||||||
|
self.sdr_color_space_var.get()
|
||||||
|
if hasattr(self, "sdr_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
),
|
||||||
|
data_range=(
|
||||||
|
self.sdr_data_range_var.get()
|
||||||
|
if hasattr(self, "sdr_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
),
|
||||||
|
bit_depth=(
|
||||||
|
self.sdr_bit_depth_var.get()
|
||||||
|
if hasattr(self, "sdr_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
),
|
||||||
|
output_format=(
|
||||||
|
self.sdr_output_format_var.get()
|
||||||
|
if hasattr(self, "sdr_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
ok = self.signal_service.update_signal_format(
|
||||||
|
color_space=(
|
||||||
|
self.hdr_color_space_var.get()
|
||||||
|
if hasattr(self, "hdr_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
),
|
||||||
|
data_range=(
|
||||||
|
self.hdr_data_range_var.get()
|
||||||
|
if hasattr(self, "hdr_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
),
|
||||||
|
bit_depth=(
|
||||||
|
self.hdr_bit_depth_var.get()
|
||||||
|
if hasattr(self, "hdr_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
),
|
||||||
|
output_format=(
|
||||||
|
self.hdr_output_format_var.get()
|
||||||
|
if hasattr(self, "hdr_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
),
|
||||||
|
max_cll=(
|
||||||
|
self.hdr_maxcll_var.get()
|
||||||
|
if hasattr(self, "hdr_maxcll_var")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
max_fall=(
|
||||||
|
self.hdr_maxfall_var.get()
|
||||||
|
if hasattr(self, "hdr_maxfall_var")
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif test_type == "local_dimming":
|
||||||
|
ok = self.signal_service.update_signal_format(
|
||||||
|
color_space=(
|
||||||
|
self.local_dimming_color_space_var.get()
|
||||||
|
if hasattr(self, "local_dimming_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
),
|
||||||
|
data_range=(
|
||||||
|
self.local_dimming_data_range_var.get()
|
||||||
|
if hasattr(self, "local_dimming_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
),
|
||||||
|
bit_depth=(
|
||||||
|
self.local_dimming_bit_depth_var.get()
|
||||||
|
if hasattr(self, "local_dimming_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
),
|
||||||
|
output_format=(
|
||||||
|
self.local_dimming_output_format_var.get()
|
||||||
|
if hasattr(self, "local_dimming_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._dispatch_ui(
|
||||||
|
self.log_gui.log,
|
||||||
|
f"Local Dimming 不支持的测试类型: {test_type}",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
self._dispatch_ui(self.log_gui.log, "Local Dimming UCD 参数设置失败", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self._dispatch_ui(self.log_gui.log, f"Local Dimming UCD 参数设置异常: {e}", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _run_ld_measurement_step(self: "PQAutomationApp", width, height, wait_time, step, log):
|
def _run_ld_measurement_step(self: "PQAutomationApp", width, height, wait_time, step, log):
|
||||||
label = step["label"]
|
label = step["label"]
|
||||||
test_item = step["test_item"]
|
test_item = step["test_item"]
|
||||||
@@ -270,6 +398,8 @@ def send_ld_window(self: "PQAutomationApp", percentage):
|
|||||||
_set_current_ld_pattern(self, "峰值亮度", f"{percentage}%窗口", percentage)
|
_set_current_ld_pattern(self, "峰值亮度", f"{percentage}%窗口", percentage)
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
|
if not _apply_ld_ucd_params(self):
|
||||||
|
return
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
try:
|
try:
|
||||||
image_path = _ensure_window_image(width, height, percentage)
|
image_path = _ensure_window_image(width, height, percentage)
|
||||||
@@ -301,6 +431,8 @@ def send_ld_checkerboard(self: "PQAutomationApp", center_white):
|
|||||||
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
|
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
|
if not _apply_ld_ucd_params(self):
|
||||||
|
return
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
try:
|
try:
|
||||||
image_path = _ensure_checkerboard_image(
|
image_path = _ensure_checkerboard_image(
|
||||||
@@ -335,6 +467,8 @@ def send_ld_black_pattern(self: "PQAutomationApp"):
|
|||||||
_set_current_ld_pattern(self, "黑电平", "全黑画面")
|
_set_current_ld_pattern(self, "黑电平", "全黑画面")
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
|
if not _apply_ld_ucd_params(self):
|
||||||
|
return
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
try:
|
try:
|
||||||
image_path = _ensure_solid_image(width, height, (0, 0, 0), "black")
|
image_path = _ensure_solid_image(width, height, (0, 0, 0), "black")
|
||||||
@@ -370,6 +504,8 @@ def send_ld_instant_peak(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
|
if not _apply_ld_ucd_params(self):
|
||||||
|
return
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
try:
|
try:
|
||||||
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
|
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
|
||||||
@@ -397,6 +533,8 @@ def send_ld_instant_peak(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
self._dispatch_ui(self.log_gui.log, msg)
|
self._dispatch_ui(self.log_gui.log, msg)
|
||||||
|
|
||||||
|
threading.Thread(target=send, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def measure_ld_luminance(self: "PQAutomationApp"):
|
def measure_ld_luminance(self: "PQAutomationApp"):
|
||||||
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
||||||
|
|||||||
@@ -1046,9 +1046,11 @@ def create_result_chart_frame(self: "PQAutomationApp"):
|
|||||||
def on_chart_tab_changed(self: "PQAutomationApp", event):
|
def on_chart_tab_changed(self: "PQAutomationApp", event):
|
||||||
"""Tab切换时的事件处理"""
|
"""Tab切换时的事件处理"""
|
||||||
try:
|
try:
|
||||||
self._last_tab_index = self.chart_notebook.index(
|
selected_tab = self.chart_notebook.select()
|
||||||
self.chart_notebook.select()
|
# 在动态 add/forget tab 的过程中,可能短暂出现“无选中页签”。
|
||||||
)
|
if not selected_tab:
|
||||||
|
return
|
||||||
|
self._last_tab_index = self.chart_notebook.index(selected_tab)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,19 @@ def show_panel(self: "PQAutomationApp", panel_name):
|
|||||||
# 显示指定面板
|
# 显示指定面板
|
||||||
panel_info = self.panels[panel_name]
|
panel_info = self.panels[panel_name]
|
||||||
|
|
||||||
# 隐藏主内容区域
|
# 隐藏主内容区域。
|
||||||
|
# Local Dimming 作为并列测试类型时,需要保留顶部配置区,
|
||||||
|
# 让用户在面板上方直接看到并修改配置项。
|
||||||
|
if panel_name == "local_dimming":
|
||||||
|
# 重新按“自适应高度”布局顶部配置区,避免其占用可扩展空间把
|
||||||
|
# Local Dimming 主面板整体向下挤出大块空白。
|
||||||
|
self.control_frame_top.pack_forget()
|
||||||
|
self.control_frame_top.pack(
|
||||||
|
side=tk.TOP, fill=tk.X, expand=False, padx=0, pady=5
|
||||||
|
)
|
||||||
|
self.control_frame_middle.pack_forget()
|
||||||
|
self.control_frame_bottom.pack_forget()
|
||||||
|
else:
|
||||||
self.control_frame_top.pack_forget()
|
self.control_frame_top.pack_forget()
|
||||||
self.control_frame_middle.pack_forget()
|
self.control_frame_middle.pack_forget()
|
||||||
self.control_frame_bottom.pack_forget()
|
self.control_frame_bottom.pack_forget()
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
self._ai_image_tooltip = None
|
self._ai_image_tooltip = None
|
||||||
self._ai_image_tooltip_label = None
|
self._ai_image_tooltip_label = None
|
||||||
self._ai_image_tooltip_item = ""
|
self._ai_image_tooltip_item = ""
|
||||||
|
# 会话级参考图 URL(图生图模式):session_id -> upload_image_url
|
||||||
|
self._ai_image_session_refs = {}
|
||||||
|
# 本轮发送前手动上传的参考图(覆盖会话级)
|
||||||
|
self._ai_image_pending_ref_url = ""
|
||||||
|
self._ai_image_pending_ref_name = ""
|
||||||
|
self._ai_image_uploading = False
|
||||||
|
self.ai_image_ref_var = tk.StringVar(value="未设置参考图(文生图模式)")
|
||||||
|
|
||||||
container = ttk.Frame(frame, padding=10)
|
container = ttk.Frame(frame, padding=10)
|
||||||
container.pack(fill=tk.BOTH, expand=True)
|
container.pack(fill=tk.BOTH, expand=True)
|
||||||
@@ -328,6 +335,29 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
input_frame = ttk.LabelFrame(right, text="提示输入(Ctrl+Enter 发送)", padding=6)
|
input_frame = ttk.LabelFrame(right, text="提示输入(Ctrl+Enter 发送)", padding=6)
|
||||||
input_frame.pack(fill=tk.X, pady=(4, 0))
|
input_frame.pack(fill=tk.X, pady=(4, 0))
|
||||||
|
|
||||||
|
# 参考图行(图生图)
|
||||||
|
ref_row = ttk.Frame(input_frame)
|
||||||
|
ref_row.pack(fill=tk.X, pady=(0, 4))
|
||||||
|
ttk.Label(
|
||||||
|
ref_row, text="参考图:",
|
||||||
|
foreground=palette["muted"], font=("微软雅黑", 9),
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
self.ai_image_ref_label = ttk.Label(
|
||||||
|
ref_row, textvariable=self.ai_image_ref_var,
|
||||||
|
foreground=palette["fg"], font=("微软雅黑", 9),
|
||||||
|
)
|
||||||
|
self.ai_image_ref_label.pack(side=tk.LEFT, padx=(4, 0), fill=tk.X, expand=True)
|
||||||
|
self.ai_image_clear_ref_btn = ttk.Button(
|
||||||
|
ref_row, text="清除", width=6, bootstyle="secondary-outline",
|
||||||
|
command=lambda: _clear_reference_image(self),
|
||||||
|
)
|
||||||
|
self.ai_image_clear_ref_btn.pack(side=tk.RIGHT, padx=(4, 0))
|
||||||
|
self.ai_image_upload_ref_btn = ttk.Button(
|
||||||
|
ref_row, text="上传参考图…", width=12, bootstyle="info-outline",
|
||||||
|
command=lambda: _upload_reference_image(self),
|
||||||
|
)
|
||||||
|
self.ai_image_upload_ref_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
self.ai_image_input = tk.Text(
|
self.ai_image_input = tk.Text(
|
||||||
input_frame,
|
input_frame,
|
||||||
height=3,
|
height=3,
|
||||||
@@ -435,6 +465,20 @@ def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
|
|||||||
|
|
||||||
self.ai_image_records = records
|
self.ai_image_records = records
|
||||||
sessions = _svc.group_records_by_session(self.ai_image_records)
|
sessions = _svc.group_records_by_session(self.ai_image_records)
|
||||||
|
# \u4f1a\u8bdd\u7ea7\u53c2\u8003\u56fe\u94fe\u8def\uff1a\u4ee5\u6bcf\u4e2a\u4f1a\u8bdd\u6700\u8fd1\u4e00\u5f20\u751f\u6210\u56fe\u7684 imageUrl \u4f5c\u4e3a\u53c2\u8003
|
||||||
|
refs_map = getattr(self, "_ai_image_session_refs", None)
|
||||||
|
if isinstance(refs_map, dict):
|
||||||
|
for sess in sessions:
|
||||||
|
sid = sess.get("session_id") or ""
|
||||||
|
if not sid or sid in refs_map:
|
||||||
|
continue
|
||||||
|
for r in sess.get("records") or []:
|
||||||
|
src = ""
|
||||||
|
if isinstance(r.extra, dict):
|
||||||
|
src = (r.extra.get("source_url") or "").strip()
|
||||||
|
if src:
|
||||||
|
refs_map[sid] = src
|
||||||
|
break
|
||||||
flat = []
|
flat = []
|
||||||
current_sid = _svc.get_session_id()
|
current_sid = _svc.get_session_id()
|
||||||
for idx, sess in enumerate(sessions, start=1):
|
for idx, sess in enumerate(sessions, start=1):
|
||||||
@@ -493,6 +537,10 @@ def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
|
|||||||
self._ai_image_list_loaded = True
|
self._ai_image_list_loaded = True
|
||||||
finally:
|
finally:
|
||||||
self._ai_image_reloading = False
|
self._ai_image_reloading = False
|
||||||
|
try:
|
||||||
|
_refresh_ref_label(self)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _format_session_header(index: int, sess: dict, is_current: bool) -> str:
|
def _format_session_header(index: int, sess: dict, is_current: bool) -> str:
|
||||||
@@ -655,6 +703,10 @@ def _start_new_session(self: "PQAutomationApp"):
|
|||||||
return
|
return
|
||||||
_svc.reset_session()
|
_svc.reset_session()
|
||||||
self.ai_image_status_var.set("已开启新对话")
|
self.ai_image_status_var.set("已开启新对话")
|
||||||
|
# 新会话清除参考图(未设置时默认为文生图模式)
|
||||||
|
self._ai_image_pending_ref_url = ""
|
||||||
|
self._ai_image_pending_ref_name = ""
|
||||||
|
_refresh_ref_label(self)
|
||||||
# 新对话创建后不要自动选中历史图片,否则会立即把 session 切回旧会话。
|
# 新对话创建后不要自动选中历史图片,否则会立即把 session 切回旧会话。
|
||||||
reload_ai_image_list(self, auto_select_first=False)
|
reload_ai_image_list(self, auto_select_first=False)
|
||||||
try:
|
try:
|
||||||
@@ -667,6 +719,103 @@ def _start_new_session(self: "PQAutomationApp"):
|
|||||||
self.ai_image_meta_var.set("新对话已开启,等待生成图片")
|
self.ai_image_meta_var.set("新对话已开启,等待生成图片")
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_pending_ref_url(self: "PQAutomationApp") -> str:
|
||||||
|
"""返回本次发送应使用的 upload_image_url。
|
||||||
|
|
||||||
|
优先使用用户在当前会话中手动上传的参考图;其次使用上一轮 imageUrl 自动链路。
|
||||||
|
"""
|
||||||
|
pending = (getattr(self, "_ai_image_pending_ref_url", "") or "").strip()
|
||||||
|
if pending:
|
||||||
|
return pending
|
||||||
|
sid = _svc.get_session_id()
|
||||||
|
refs = getattr(self, "_ai_image_session_refs", None) or {}
|
||||||
|
return (refs.get(sid) or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_ref_label(self: "PQAutomationApp"):
|
||||||
|
"""根据当前 pending 上传 / 会话链路状态刷新参考图提示标签。"""
|
||||||
|
var = getattr(self, "ai_image_ref_var", None)
|
||||||
|
if var is None:
|
||||||
|
return
|
||||||
|
pending = (getattr(self, "_ai_image_pending_ref_url", "") or "").strip()
|
||||||
|
if pending:
|
||||||
|
name = getattr(self, "_ai_image_pending_ref_name", "") or "参考图"
|
||||||
|
var.set(f"[图生图] {name}")
|
||||||
|
return
|
||||||
|
sid = _svc.get_session_id()
|
||||||
|
refs = getattr(self, "_ai_image_session_refs", None) or {}
|
||||||
|
chained = (refs.get(sid) or "").strip()
|
||||||
|
if chained:
|
||||||
|
# 自动链路:显示来源是"上一轮"
|
||||||
|
var.set("[图生图] 沿用上一轮生成图为参考")
|
||||||
|
return
|
||||||
|
var.set("未设置参考图(文生图模式)")
|
||||||
|
|
||||||
|
|
||||||
|
def _upload_reference_image(self: "PQAutomationApp"):
|
||||||
|
"""选择本地图片并上传到后端,成功后作为本次发送的参考图。"""
|
||||||
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
|
messagebox.showinfo("提示", "请等待当前请求完成")
|
||||||
|
return
|
||||||
|
if getattr(self, "_ai_image_uploading", False):
|
||||||
|
return
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="选择参考图(PNG/JPG/JPEG,超过限制将自动缩放)",
|
||||||
|
filetypes=[("图片", "*.png;*.jpg;*.jpeg"), ("所有文件", "*.*")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._ai_image_uploading = True
|
||||||
|
try:
|
||||||
|
self.ai_image_upload_ref_btn.configure(state=tk.DISABLED, text="上传中…")
|
||||||
|
self.ai_image_clear_ref_btn.configure(state=tk.DISABLED)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.ai_image_status_var.set("正在上传参考图…")
|
||||||
|
|
||||||
|
name = os.path.basename(path)
|
||||||
|
|
||||||
|
def _ok(url: str):
|
||||||
|
self.root.after(0, lambda: _on_upload_done(self, name, url, None))
|
||||||
|
|
||||||
|
def _err(exc: Exception):
|
||||||
|
self.root.after(0, lambda: _on_upload_done(self, name, "", exc))
|
||||||
|
|
||||||
|
_svc.upload_image_async(path, on_success=_ok, on_error=_err)
|
||||||
|
|
||||||
|
|
||||||
|
def _on_upload_done(self: "PQAutomationApp", name: str, url: str, exc):
|
||||||
|
self._ai_image_uploading = False
|
||||||
|
try:
|
||||||
|
self.ai_image_upload_ref_btn.configure(state=tk.NORMAL, text="上传参考图…")
|
||||||
|
self.ai_image_clear_ref_btn.configure(state=tk.NORMAL)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if exc is not None:
|
||||||
|
self.ai_image_status_var.set(f"上传失败: {exc}")
|
||||||
|
messagebox.showerror("上传失败", str(exc))
|
||||||
|
return
|
||||||
|
self._ai_image_pending_ref_url = url
|
||||||
|
self._ai_image_pending_ref_name = name
|
||||||
|
_refresh_ref_label(self)
|
||||||
|
self.ai_image_status_var.set(f"参考图已上传:{name}")
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_reference_image(self: "PQAutomationApp"):
|
||||||
|
"""清除手动上传的参考图,同时清除当前会话的自动链路参考。"""
|
||||||
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
|
return
|
||||||
|
self._ai_image_pending_ref_url = ""
|
||||||
|
self._ai_image_pending_ref_name = ""
|
||||||
|
sid = _svc.get_session_id()
|
||||||
|
refs = getattr(self, "_ai_image_session_refs", None)
|
||||||
|
if isinstance(refs, dict):
|
||||||
|
refs.pop(sid, None)
|
||||||
|
_refresh_ref_label(self)
|
||||||
|
self.ai_image_status_var.set("已清除参考图,切换为文生图模式")
|
||||||
|
|
||||||
|
|
||||||
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
||||||
session_map = getattr(self, "_ai_image_session_node_map", None) or {}
|
session_map = getattr(self, "_ai_image_session_node_map", None) or {}
|
||||||
parent = item_id
|
parent = item_id
|
||||||
@@ -704,6 +853,10 @@ def _switch_to_session(
|
|||||||
if item_id:
|
if item_id:
|
||||||
_set_tree_selection(self, item_id)
|
_set_tree_selection(self, item_id)
|
||||||
self.ai_image_status_var.set("已切换到历史对话")
|
self.ai_image_status_var.set("已切换到历史对话")
|
||||||
|
# 切换会话后刷新参考图标签(pending 仅当前会话有效,故清除)
|
||||||
|
self._ai_image_pending_ref_url = ""
|
||||||
|
self._ai_image_pending_ref_name = ""
|
||||||
|
_refresh_ref_label(self)
|
||||||
if show_message:
|
if show_message:
|
||||||
messagebox.showinfo("提示", "已切换到所选历史对话")
|
messagebox.showinfo("提示", "已切换到所选历史对话")
|
||||||
|
|
||||||
@@ -720,6 +873,9 @@ def _update_request_progress(self: "PQAutomationApp"):
|
|||||||
def _send_prompt(self: "PQAutomationApp"):
|
def _send_prompt(self: "PQAutomationApp"):
|
||||||
if getattr(self, "_ai_image_requesting", False):
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
return
|
return
|
||||||
|
if getattr(self, "_ai_image_uploading", False):
|
||||||
|
messagebox.showinfo("提示", "参考图上传中,请稍后再发送")
|
||||||
|
return
|
||||||
prompt = self.ai_image_input.get("1.0", tk.END).strip()
|
prompt = self.ai_image_input.get("1.0", tk.END).strip()
|
||||||
if not prompt:
|
if not prompt:
|
||||||
messagebox.showinfo("提示", "请输入内容")
|
messagebox.showinfo("提示", "请输入内容")
|
||||||
@@ -749,12 +905,17 @@ def _send_prompt(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
ref_url = _resolve_pending_ref_url(self)
|
||||||
|
if ref_url:
|
||||||
|
self.ai_image_status_var.set("后端处理中(图生图)…")
|
||||||
|
|
||||||
_svc.request_image_async(
|
_svc.request_image_async(
|
||||||
prompt,
|
prompt,
|
||||||
on_success=_success,
|
on_success=_success,
|
||||||
on_error=_error,
|
on_error=_error,
|
||||||
base_dir=_get_app_base_dir(self),
|
base_dir=_get_app_base_dir(self),
|
||||||
cancel_event=self._ai_image_cancel_event,
|
cancel_event=self._ai_image_cancel_event,
|
||||||
|
upload_image_url=ref_url or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -764,6 +925,10 @@ def _set_requesting(self: "PQAutomationApp", flag: bool):
|
|||||||
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
self.ai_image_new_session_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
self.ai_image_new_session_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
self.ai_image_stop_btn.configure(state=tk.NORMAL if flag else tk.DISABLED)
|
self.ai_image_stop_btn.configure(state=tk.NORMAL if flag else tk.DISABLED)
|
||||||
|
if hasattr(self, "ai_image_upload_ref_btn"):
|
||||||
|
self.ai_image_upload_ref_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
|
if hasattr(self, "ai_image_clear_ref_btn"):
|
||||||
|
self.ai_image_clear_ref_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if flag:
|
if flag:
|
||||||
@@ -797,9 +962,24 @@ def _on_request_done(self: "PQAutomationApp", record, exc, req_seq):
|
|||||||
return
|
return
|
||||||
self.ai_image_status_var.set("完成")
|
self.ai_image_status_var.set("完成")
|
||||||
self.ai_image_input.delete("1.0", tk.END)
|
self.ai_image_input.delete("1.0", tk.END)
|
||||||
|
# 多轮对话链路:本轮返回的 imageUrl 作为下一轮的参考图
|
||||||
|
next_ref = ""
|
||||||
|
try:
|
||||||
|
if record is not None and isinstance(record.extra, dict):
|
||||||
|
next_ref = (record.extra.get("source_url") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
next_ref = ""
|
||||||
|
sid = (record.session_id if record is not None else "") or _svc.get_session_id()
|
||||||
|
if next_ref and sid:
|
||||||
|
self._ai_image_session_refs[sid] = next_ref
|
||||||
|
# 手动上传的 pending 参考图只对本次发送生效,发完清除
|
||||||
|
self._ai_image_pending_ref_url = ""
|
||||||
|
self._ai_image_pending_ref_name = ""
|
||||||
|
_refresh_ref_label(self)
|
||||||
reload_ai_image_list(self)
|
reload_ai_image_list(self)
|
||||||
if record is not None:
|
if record is not None:
|
||||||
logger.info("[AIImagePanel] 生成完成并入列 id=%s sid=%s", record.id, (record.session_id or "")[:8])
|
logger.info("[AIImagePanel] 生成完成并入列 id=%s sid=%s next_ref=%s",
|
||||||
|
record.id, (record.session_id or "")[:8], next_ref or "-")
|
||||||
if record is not None and self.ai_image_records:
|
if record is not None and self.ai_image_records:
|
||||||
item_id = _find_tree_item_by_record_id(self, record.id)
|
item_id = _find_tree_item_by_record_id(self, record.id)
|
||||||
if item_id:
|
if item_id:
|
||||||
|
|||||||
@@ -36,13 +36,7 @@ def _make_card(parent, icon: str, title: str) -> ttk.Frame:
|
|||||||
|
|
||||||
|
|
||||||
def create_floating_config_panel(self: "PQAutomationApp"):
|
def create_floating_config_panel(self: "PQAutomationApp"):
|
||||||
"""创建顶部"配置项"现代化折叠面板。
|
"""创建顶部"配置项"现代化折叠面板
|
||||||
|
|
||||||
布局变化(vs 旧版):
|
|
||||||
- 用 Unicode chevron + 整条 header 可点击折叠/展开;
|
|
||||||
- header 上额外显示折叠状态预览(``config_preview_var``);
|
|
||||||
- header 右侧承载常驻操作工具条(开始/停止/保存 等),不再放中部;
|
|
||||||
- 内部三个区段从 LabelFrame 改为统一的 Card 样式。
|
|
||||||
"""
|
"""
|
||||||
cf = CollapsingFrame(self.control_frame_top)
|
cf = CollapsingFrame(self.control_frame_top)
|
||||||
cf.pack(fill="both")
|
cf.pack(fill="both")
|
||||||
@@ -54,8 +48,7 @@ def create_floating_config_panel(self: "PQAutomationApp"):
|
|||||||
# 折叠预览:呈现"测试类型 · 已选测试项"
|
# 折叠预览:呈现"测试类型 · 已选测试项"
|
||||||
self.config_preview_var = tk.StringVar(value="")
|
self.config_preview_var = tk.StringVar(value="")
|
||||||
|
|
||||||
# header 右侧工具条占位 —— create_operation_frame 之后向这里挂按钮
|
self.toolbar_actions_frame = None
|
||||||
self.toolbar_actions_frame: ttk.Frame | None = None
|
|
||||||
|
|
||||||
def _header_actions(parent: ttk.Frame):
|
def _header_actions(parent: ttk.Frame):
|
||||||
# 暴露给 create_operation_frame 使用
|
# 暴露给 create_operation_frame 使用
|
||||||
@@ -90,7 +83,7 @@ def create_floating_config_panel(self: "PQAutomationApp"):
|
|||||||
signal_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
signal_card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
||||||
self.signal_format_frame = signal_card._body # type: ignore[attr-defined]
|
self.signal_format_frame = signal_card._body # type: ignore[attr-defined]
|
||||||
|
|
||||||
# 创建卡片内部内容(沿用旧函数,父级已是 body Frame)
|
# 创建卡片内部内容
|
||||||
self.create_connection_content()
|
self.create_connection_content()
|
||||||
self.create_test_items_content()
|
self.create_test_items_content()
|
||||||
self.create_signal_format_content()
|
self.create_signal_format_content()
|
||||||
@@ -111,6 +104,7 @@ def refresh_config_preview(self: "PQAutomationApp") -> None:
|
|||||||
"screen_module": "屏模组",
|
"screen_module": "屏模组",
|
||||||
"sdr_movie": "SDR Movie",
|
"sdr_movie": "SDR Movie",
|
||||||
"hdr_movie": "HDR Movie",
|
"hdr_movie": "HDR Movie",
|
||||||
|
"local_dimming": "Local Dimming",
|
||||||
}
|
}
|
||||||
current_type = getattr(self.config, "current_test_type", "")
|
current_type = getattr(self.config, "current_test_type", "")
|
||||||
type_label = type_labels.get(current_type, "")
|
type_label = type_labels.get(current_type, "")
|
||||||
@@ -171,6 +165,10 @@ def create_test_items_content(self: "PQAutomationApp"):
|
|||||||
("色准", "accuracy"),
|
("色准", "accuracy"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"local_dimming": {
|
||||||
|
"frame": ttk.Frame(self.test_items_frame),
|
||||||
|
"items": [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 根据当前测试类型创建复选框
|
# 根据当前测试类型创建复选框
|
||||||
@@ -473,10 +471,103 @@ def create_signal_format_content(self: "PQAutomationApp"):
|
|||||||
hdr_output_format_combo.bind("<<ComboboxSelected>>", self.on_hdr_output_format_changed)
|
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)
|
hdr_output_format_combo.grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# ==================== Local Dimming 信号格式设置 ====================
|
||||||
|
self.local_dimming_signal_frame = ttk.Frame(self.signal_tabs)
|
||||||
|
self.local_dimming_signal_frame.grid_columnconfigure(0, weight=0)
|
||||||
|
self.local_dimming_signal_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
self.signal_tabs.add(self.local_dimming_signal_frame, text="Local Dimming")
|
||||||
|
|
||||||
|
ld_cfg = self.config.current_test_types.get("local_dimming", {})
|
||||||
|
|
||||||
|
ttk.Label(self.local_dimming_signal_frame, text="分辨率:").grid(
|
||||||
|
row=0, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.local_dimming_timing_var = tk.StringVar(
|
||||||
|
value=ld_cfg.get("timing", "DMT 1920x 1080 @ 60Hz")
|
||||||
|
)
|
||||||
|
ld_timing_combo = ttk.Combobox(
|
||||||
|
self.local_dimming_signal_frame,
|
||||||
|
textvariable=self.local_dimming_timing_var,
|
||||||
|
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
|
||||||
|
width=20,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ld_timing_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_timing_changed)
|
||||||
|
ld_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Label(self.local_dimming_signal_frame, text="色彩空间:").grid(
|
||||||
|
row=1, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.local_dimming_color_space_var = tk.StringVar(
|
||||||
|
value=ld_cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
ld_color_space_combo = ttk.Combobox(
|
||||||
|
self.local_dimming_signal_frame,
|
||||||
|
textvariable=self.local_dimming_color_space_var,
|
||||||
|
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ld_color_space_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
|
||||||
|
ld_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Label(self.local_dimming_signal_frame, text="数据范围:").grid(
|
||||||
|
row=2, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.local_dimming_data_range_var = tk.StringVar(
|
||||||
|
value=ld_cfg.get("data_range", UCDEnum.SignalFormat.DataRange.FULL)
|
||||||
|
)
|
||||||
|
ld_data_range_combo = ttk.Combobox(
|
||||||
|
self.local_dimming_signal_frame,
|
||||||
|
textvariable=self.local_dimming_data_range_var,
|
||||||
|
values=UCDEnum.SignalFormat.DataRange.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ld_data_range_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
|
||||||
|
ld_data_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
default_ld_bpc = int(ld_cfg.get("bpc", 8))
|
||||||
|
default_ld_bit_depth = (
|
||||||
|
f"{default_ld_bpc}bit"
|
||||||
|
if f"{default_ld_bpc}bit" in UCDEnum.SignalFormat.BitDepth.get_list()
|
||||||
|
else UCDEnum.SignalFormat.BitDepth.BIT_8
|
||||||
|
)
|
||||||
|
ttk.Label(self.local_dimming_signal_frame, text="编码位深:").grid(
|
||||||
|
row=3, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.local_dimming_bit_depth_var = tk.StringVar(value=default_ld_bit_depth)
|
||||||
|
ld_bit_depth_combo = ttk.Combobox(
|
||||||
|
self.local_dimming_signal_frame,
|
||||||
|
textvariable=self.local_dimming_bit_depth_var,
|
||||||
|
values=UCDEnum.SignalFormat.BitDepth.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ld_bit_depth_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
|
||||||
|
ld_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
ttk.Label(self.local_dimming_signal_frame, text="色彩格式:").grid(
|
||||||
|
row=4, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.local_dimming_output_format_var = tk.StringVar(
|
||||||
|
value=ld_cfg.get("color_format", UCDEnum.SignalFormat.OutputFormat.RGB)
|
||||||
|
)
|
||||||
|
ld_output_format_combo = ttk.Combobox(
|
||||||
|
self.local_dimming_signal_frame,
|
||||||
|
textvariable=self.local_dimming_output_format_var,
|
||||||
|
values=UCDEnum.SignalFormat.OutputFormat.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ld_output_format_combo.bind("<<ComboboxSelected>>", self.on_local_dimming_signal_format_changed)
|
||||||
|
ld_output_format_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
# ==================== 初始化:默认只启用屏模组 Tab ====================
|
# ==================== 初始化:默认只启用屏模组 Tab ====================
|
||||||
self.signal_tabs.select(0) # 选中屏模组
|
self.signal_tabs.select(0) # 选中屏模组
|
||||||
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
|
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
|
||||||
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
|
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
|
||||||
|
self.signal_tabs.tab(3, state="disabled") # 禁用 Local Dimming
|
||||||
|
|
||||||
|
|
||||||
def create_connection_content(self: "PQAutomationApp"):
|
def create_connection_content(self: "PQAutomationApp"):
|
||||||
@@ -513,41 +604,50 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 添加按钮框架
|
# 添加按钮框架
|
||||||
button_frame = ttk.Frame(com_frame)
|
button_frame = ttk.Frame(com_frame)
|
||||||
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="w")
|
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="ew")
|
||||||
|
button_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
button_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
button_frame.grid_columnconfigure(2, weight=1)
|
||||||
|
|
||||||
connect_icon = load_icon("assets/connect-svgrepo-com.png")
|
# connect_icon = load_icon("assets/connect-svgrepo-com.png")
|
||||||
self.check_button = ttk.Button(
|
self.check_button = ttk.Button(
|
||||||
button_frame,
|
button_frame,
|
||||||
image=connect_icon,
|
# image=connect_icon,
|
||||||
bootstyle="link",
|
# bootstyle="link",
|
||||||
|
text="连接",
|
||||||
|
bootstyle="success",
|
||||||
takefocus=False,
|
takefocus=False,
|
||||||
command=self.check_com_connections,
|
command=self.check_com_connections,
|
||||||
)
|
)
|
||||||
self.check_button.image = connect_icon
|
# self.check_button.image = connect_icon
|
||||||
self.check_button.pack(side="left", padx=0, pady=3)
|
self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew")
|
||||||
|
|
||||||
disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
|
# disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
|
||||||
# 断开连接按钮
|
# 断开连接按钮
|
||||||
self.disconnect_button = ttk.Button(
|
self.disconnect_button = ttk.Button(
|
||||||
button_frame,
|
button_frame,
|
||||||
image=disconnect_icon,
|
# image=disconnect_icon,
|
||||||
bootstyle="link",
|
# bootstyle="link",
|
||||||
|
text="断开",
|
||||||
|
bootstyle="danger",
|
||||||
takefocus=False,
|
takefocus=False,
|
||||||
command=self.disconnect_com_connections,
|
command=self.disconnect_com_connections,
|
||||||
)
|
)
|
||||||
self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
|
# self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
|
||||||
self.disconnect_button.pack(side="left", padx=0, pady=3)
|
self.disconnect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew")
|
||||||
|
|
||||||
refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
|
# refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
|
||||||
self.refresh_button = ttk.Button(
|
self.refresh_button = ttk.Button(
|
||||||
button_frame,
|
button_frame,
|
||||||
image=refresh_icon,
|
# image=refresh_icon,
|
||||||
bootstyle="link",
|
# bootstyle="link",
|
||||||
|
text="刷新",
|
||||||
|
bootstyle="info",
|
||||||
takefocus=False,
|
takefocus=False,
|
||||||
command=self.refresh_com_ports,
|
command=self.refresh_com_ports,
|
||||||
)
|
)
|
||||||
self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
|
# self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
|
||||||
self.refresh_button.pack(side="left", padx=0, pady=3)
|
self.refresh_button.grid(row=0, column=2, padx=(4, 0), pady=3, sticky="ew")
|
||||||
|
|
||||||
# CA端口
|
# CA端口
|
||||||
ttk.Label(com_frame, text="CA端口:").grid(
|
ttk.Label(com_frame, text="CA端口:").grid(
|
||||||
@@ -591,12 +691,6 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
def create_test_type_frame(self: "PQAutomationApp"):
|
def create_test_type_frame(self: "PQAutomationApp"):
|
||||||
"""创建测试类型选择区域(侧边栏形式)。
|
"""创建测试类型选择区域(侧边栏形式)。
|
||||||
|
|
||||||
新版(v3)改进:
|
|
||||||
- 深灰分层背景,接近 Calman 的侧栏密度;
|
|
||||||
- 纯文字按钮,不使用 emoji;
|
|
||||||
- 用更克制的字号 / 间距做层级区分;
|
|
||||||
- 不再使用 padding=10 硬覆盖(交给 Sidebar.TButton 样式统一管理)。
|
|
||||||
"""
|
"""
|
||||||
# 设置测试类型变量
|
# 设置测试类型变量
|
||||||
self.test_type_var = tk.StringVar(value="screen_module")
|
self.test_type_var = tk.StringVar(value="screen_module")
|
||||||
@@ -621,6 +715,7 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
("屏模组性能测试", "screen_module"),
|
("屏模组性能测试", "screen_module"),
|
||||||
("SDR Movie", "sdr_movie"),
|
("SDR Movie", "sdr_movie"),
|
||||||
("HDR Movie", "hdr_movie"),
|
("HDR Movie", "hdr_movie"),
|
||||||
|
("Local Dimming", "local_dimming"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for text, type_value in test_types:
|
for text, type_value in test_types:
|
||||||
@@ -643,7 +738,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
panel_buttons = [
|
panel_buttons = [
|
||||||
("log_btn", "测试日志", self.toggle_log_panel),
|
("log_btn", "测试日志", self.toggle_log_panel),
|
||||||
("local_dimming_btn", "Local Dimming", self.toggle_local_dimming_panel),
|
|
||||||
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
|
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
|
||||||
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
||||||
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel),
|
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel),
|
||||||
@@ -688,8 +782,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
if hasattr(self, "panels"):
|
if hasattr(self, "panels"):
|
||||||
if "log" in self.panels:
|
if "log" in self.panels:
|
||||||
self.panels["log"]["button"] = self.log_btn
|
self.panels["log"]["button"] = self.log_btn
|
||||||
if "local_dimming" in self.panels:
|
|
||||||
self.panels["local_dimming"]["button"] = self.local_dimming_btn
|
|
||||||
if "ai_image" in self.panels:
|
if "ai_image" in self.panels:
|
||||||
self.panels["ai_image"]["button"] = self.ai_image_btn
|
self.panels["ai_image"]["button"] = self.ai_image_btn
|
||||||
if "single_step" in self.panels:
|
if "single_step" in self.panels:
|
||||||
@@ -785,16 +877,6 @@ def create_operation_frame(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
self.start_btn.pack(side=tk.LEFT, **btn_pad)
|
self.start_btn.pack(side=tk.LEFT, **btn_pad)
|
||||||
|
|
||||||
self.simulate_btn = ttk.Button(
|
|
||||||
parent,
|
|
||||||
text="模拟测试",
|
|
||||||
command=self.run_simulation_test,
|
|
||||||
bootstyle="warning-outline",
|
|
||||||
padding=(12, 6),
|
|
||||||
takefocus=False,
|
|
||||||
)
|
|
||||||
self.simulate_btn.pack(side=tk.LEFT, **btn_pad)
|
|
||||||
|
|
||||||
self.stop_btn = ttk.Button(
|
self.stop_btn = ttk.Button(
|
||||||
parent,
|
parent,
|
||||||
text="\u25a0 停止",
|
text="\u25a0 停止",
|
||||||
@@ -995,6 +1077,65 @@ def on_hdr_output_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
|
self.log_gui.log(f"HDR色彩格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def on_local_dimming_timing_changed(self: "PQAutomationApp", event=None):
|
||||||
|
"""Local Dimming 分辨率改变时的回调。"""
|
||||||
|
try:
|
||||||
|
selected_timing = self.local_dimming_timing_var.get()
|
||||||
|
self.log_gui.log(f"Local Dimming 分辨率已更改为: {selected_timing}", level="info")
|
||||||
|
|
||||||
|
self.config.current_test_types.setdefault("local_dimming", {})["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"Local Dimming 分辨率更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def on_local_dimming_signal_format_changed(self: "PQAutomationApp", event=None):
|
||||||
|
"""Local Dimming ColorInfo 相关选项变更回调。"""
|
||||||
|
try:
|
||||||
|
color_space = self.local_dimming_color_space_var.get()
|
||||||
|
data_range = self.local_dimming_data_range_var.get()
|
||||||
|
bit_depth = self.local_dimming_bit_depth_var.get()
|
||||||
|
output_format = self.local_dimming_output_format_var.get()
|
||||||
|
|
||||||
|
ld_cfg = self.config.current_test_types.setdefault("local_dimming", {})
|
||||||
|
ld_cfg["colorimetry"] = color_space
|
||||||
|
ld_cfg["color_format"] = output_format
|
||||||
|
ld_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
||||||
|
ld_cfg["data_range"] = data_range
|
||||||
|
|
||||||
|
self.log_gui.log(
|
||||||
|
(
|
||||||
|
"Local Dimming 信号格式已更新: "
|
||||||
|
f"色彩空间={color_space}, 数据范围={data_range}, "
|
||||||
|
f"位深={bit_depth}, 色彩格式={output_format}"
|
||||||
|
),
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.testing:
|
||||||
|
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
||||||
|
self.save_pq_config()
|
||||||
|
return
|
||||||
|
|
||||||
|
if getattr(self.ucd, "status", False):
|
||||||
|
ok = self.signal_service.update_signal_format(
|
||||||
|
color_space=color_space,
|
||||||
|
data_range=data_range,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
output_format=output_format,
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
self.log_gui.log("Local Dimming 信号格式应用到UCD失败", level="error")
|
||||||
|
|
||||||
|
self.save_pq_config()
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"Local Dimming 信号格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def update_test_items(self: "PQAutomationApp"):
|
def update_test_items(self: "PQAutomationApp"):
|
||||||
"""根据当前测试类型更新测试项目复选框"""
|
"""根据当前测试类型更新测试项目复选框"""
|
||||||
# 先隐藏所有测试项目框架
|
# 先隐藏所有测试项目框架
|
||||||
@@ -1023,6 +1164,7 @@ def update_test_items(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 添加复选框
|
# 添加复选框
|
||||||
|
toggle_bootstyle = "success-round-toggle"
|
||||||
for i, (text, var_name) in enumerate(config["items"]):
|
for i, (text, var_name) in enumerate(config["items"]):
|
||||||
is_checked = var_name in saved_test_items
|
is_checked = var_name in saved_test_items
|
||||||
var = tk.BooleanVar(value=is_checked)
|
var = tk.BooleanVar(value=is_checked)
|
||||||
@@ -1032,7 +1174,7 @@ def update_test_items(self: "PQAutomationApp"):
|
|||||||
frame,
|
frame,
|
||||||
text=text,
|
text=text,
|
||||||
variable=var,
|
variable=var,
|
||||||
bootstyle="round-toggle",
|
bootstyle=toggle_bootstyle,
|
||||||
command=self.update_config_and_tabs,
|
command=self.update_config_and_tabs,
|
||||||
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
|
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
|
||||||
|
|
||||||
@@ -1054,6 +1196,12 @@ def on_test_type_change(self: "PQAutomationApp"):
|
|||||||
# SDR 选中时显示客户模版按钮
|
# SDR 选中时显示客户模版按钮
|
||||||
self.update_custom_button_visibility()
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
# Local Dimming 作为并列测试类型时,自动显示其专用面板。
|
||||||
|
if self.config.current_test_type == "local_dimming":
|
||||||
|
self.show_panel("local_dimming")
|
||||||
|
elif getattr(self, "current_panel", None) == "local_dimming":
|
||||||
|
self.hide_all_panels()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MainLayoutMixin:
|
class MainLayoutMixin:
|
||||||
@@ -1074,5 +1222,7 @@ class MainLayoutMixin:
|
|||||||
on_sdr_timing_changed = on_sdr_timing_changed
|
on_sdr_timing_changed = on_sdr_timing_changed
|
||||||
on_sdr_output_format_changed = on_sdr_output_format_changed
|
on_sdr_output_format_changed = on_sdr_output_format_changed
|
||||||
on_hdr_output_format_changed = on_hdr_output_format_changed
|
on_hdr_output_format_changed = on_hdr_output_format_changed
|
||||||
|
on_local_dimming_timing_changed = on_local_dimming_timing_changed
|
||||||
|
on_local_dimming_signal_format_changed = on_local_dimming_signal_format_changed
|
||||||
update_test_items = update_test_items
|
update_test_items = update_test_items
|
||||||
on_test_type_change = on_test_type_change
|
on_test_type_change = on_test_type_change
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ def update_sidebar_selection(self: "PQAutomationApp"):
|
|||||||
self.screen_module_btn.configure(style="Sidebar.TButton")
|
self.screen_module_btn.configure(style="Sidebar.TButton")
|
||||||
self.sdr_movie_btn.configure(style="Sidebar.TButton")
|
self.sdr_movie_btn.configure(style="Sidebar.TButton")
|
||||||
self.hdr_movie_btn.configure(style="Sidebar.TButton")
|
self.hdr_movie_btn.configure(style="Sidebar.TButton")
|
||||||
|
self.local_dimming_btn.configure(style="Sidebar.TButton")
|
||||||
|
|
||||||
# 设置当前选中按钮的样式
|
# 设置当前选中按钮的样式
|
||||||
current_type = self.test_type_var.get()
|
current_type = self.test_type_var.get()
|
||||||
@@ -374,6 +375,8 @@ def update_sidebar_selection(self: "PQAutomationApp"):
|
|||||||
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
|
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
elif current_type == "hdr_movie":
|
elif current_type == "hdr_movie":
|
||||||
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
|
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
elif current_type == "local_dimming":
|
||||||
|
self.local_dimming_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
|
||||||
|
|
||||||
class SidePanelsMixin:
|
class SidePanelsMixin:
|
||||||
|
|||||||
BIN
docs/PQ生图后端接口文档v2.pdf
Normal file
BIN
docs/PQ生图后端接口文档v2.pdf
Normal file
Binary file not shown.
Binary file not shown.
@@ -358,10 +358,11 @@ class PQAutomationApp(
|
|||||||
"screen_module": 0,
|
"screen_module": 0,
|
||||||
"sdr_movie": 1,
|
"sdr_movie": 1,
|
||||||
"hdr_movie": 2,
|
"hdr_movie": 2,
|
||||||
|
"local_dimming": 3,
|
||||||
}
|
}
|
||||||
target_tab = tab_mapping.get(test_type, 0)
|
target_tab = tab_mapping.get(test_type, 0)
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(4):
|
||||||
self.signal_tabs.tab(i, state="normal")
|
self.signal_tabs.tab(i, state="normal")
|
||||||
|
|
||||||
self.signal_tabs.select(target_tab)
|
self.signal_tabs.select(target_tab)
|
||||||
@@ -374,8 +375,10 @@ class PQAutomationApp(
|
|||||||
self.sdr_signal_frame.tkraise()
|
self.sdr_signal_frame.tkraise()
|
||||||
elif target_tab == 2:
|
elif target_tab == 2:
|
||||||
self.hdr_signal_frame.tkraise()
|
self.hdr_signal_frame.tkraise()
|
||||||
|
elif target_tab == 3:
|
||||||
|
self.local_dimming_signal_frame.tkraise()
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(4):
|
||||||
if i != target_tab:
|
if i != target_tab:
|
||||||
self.signal_tabs.tab(i, state="disabled")
|
self.signal_tabs.tab(i, state="disabled")
|
||||||
|
|
||||||
@@ -397,16 +400,16 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
if test_type == "hdr_movie":
|
if test_type == "hdr_movie":
|
||||||
if gamma_tab_id in current_tabs:
|
if gamma_tab_id in current_tabs:
|
||||||
gamma_index = current_tabs.index(gamma_tab_id)
|
self.chart_notebook.forget(self.gamma_chart_frame)
|
||||||
self.chart_notebook.forget(gamma_index)
|
|
||||||
if eotf_tab_id not in current_tabs:
|
if eotf_tab_id not in current_tabs:
|
||||||
self.chart_notebook.insert(1, self.eotf_chart_frame, text="EOTF 曲线")
|
insert_pos = min(1, len(self.chart_notebook.tabs()))
|
||||||
|
self.chart_notebook.insert(insert_pos, self.eotf_chart_frame, text="EOTF 曲线")
|
||||||
else:
|
else:
|
||||||
if eotf_tab_id in current_tabs:
|
if eotf_tab_id in current_tabs:
|
||||||
eotf_index = current_tabs.index(eotf_tab_id)
|
self.chart_notebook.forget(self.eotf_chart_frame)
|
||||||
self.chart_notebook.forget(eotf_index)
|
|
||||||
if gamma_tab_id not in current_tabs:
|
if gamma_tab_id not in current_tabs:
|
||||||
self.chart_notebook.insert(1, self.gamma_chart_frame, text="Gamma 曲线")
|
insert_pos = min(1, len(self.chart_notebook.tabs()))
|
||||||
|
self.chart_notebook.insert(insert_pos, self.gamma_chart_frame, text="Gamma 曲线")
|
||||||
|
|
||||||
custom_tab_id = str(self.custom_template_tab_frame)
|
custom_tab_id = str(self.custom_template_tab_frame)
|
||||||
current_tabs = list(self.chart_notebook.tabs())
|
current_tabs = list(self.chart_notebook.tabs())
|
||||||
@@ -533,6 +536,8 @@ class PQAutomationApp(
|
|||||||
return "开始 SDR Movie 测试,请设置正确的图像模式"
|
return "开始 SDR Movie 测试,请设置正确的图像模式"
|
||||||
if test_type == "hdr_movie":
|
if test_type == "hdr_movie":
|
||||||
return "开始 HDR Movie 测试,请设置正确的图像模式"
|
return "开始 HDR Movie 测试,请设置正确的图像模式"
|
||||||
|
if test_type == "local_dimming":
|
||||||
|
return "Local Dimming 为手动模式,请在右侧面板发送图案并采集数据"
|
||||||
return f"开始{self.get_test_type_name(test_type)}测试"
|
return f"开始{self.get_test_type_name(test_type)}测试"
|
||||||
|
|
||||||
def _launch_test_thread(self, test_type, test_items):
|
def _launch_test_thread(self, test_type, test_items):
|
||||||
@@ -743,6 +748,8 @@ class PQAutomationApp(
|
|||||||
return "SDR Movie测试"
|
return "SDR Movie测试"
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
return "HDR Movie测试"
|
return "HDR Movie测试"
|
||||||
|
elif test_type == "local_dimming":
|
||||||
|
return "Local Dimming"
|
||||||
return test_type
|
return test_type
|
||||||
|
|
||||||
def get_selected_test_items(self):
|
def get_selected_test_items(self):
|
||||||
@@ -772,6 +779,11 @@ class PQAutomationApp(
|
|||||||
and hasattr(self, "sdr_timing_var")
|
and hasattr(self, "sdr_timing_var")
|
||||||
):
|
):
|
||||||
self.config.set_current_timing(self.sdr_timing_var.get())
|
self.config.set_current_timing(self.sdr_timing_var.get())
|
||||||
|
elif (
|
||||||
|
self.config.current_test_type == "local_dimming"
|
||||||
|
and hasattr(self, "local_dimming_timing_var")
|
||||||
|
):
|
||||||
|
self.config.set_current_timing(self.local_dimming_timing_var.get())
|
||||||
# 自动保存配置到文件
|
# 自动保存配置到文件
|
||||||
self.save_pq_config()
|
self.save_pq_config()
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,16 @@
|
|||||||
"y_ideal": 0.329,
|
"y_ideal": 0.329,
|
||||||
"y_tolerance": 0.003
|
"y_tolerance": 0.003
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"local_dimming": {
|
||||||
|
"name": "Local Dimming",
|
||||||
|
"test_items": [],
|
||||||
|
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||||
|
"data_range": "Full",
|
||||||
|
"color_format": "RGB",
|
||||||
|
"bpc": 8,
|
||||||
|
"colorimetry": "sRGB",
|
||||||
|
"patterns": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"device_config": {
|
"device_config": {
|
||||||
|
|||||||
Reference in New Issue
Block a user