修改AI生图接口、修改设备连接UI、修改LocalDimming逻辑和UI

This commit is contained in:
xinzhu.yin
2026-05-29 14:40:39 +08:00
parent 21455f3916
commit 85ac47e8de
13 changed files with 811 additions and 304 deletions

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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()

View File

@@ -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。"""

View File

@@ -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")

View File

@@ -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()

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View File

@@ -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": {