From 64764524aa1e4a409d0864974918a9367bb2d6d9 Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Thu, 28 May 2026 17:02:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Local=20Dimming=E5=9B=BE?= =?UTF-8?q?=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/local_dimming.py | 360 ++++++++++++++++++++++++-------- app/views/panels/side_panels.py | 60 +++++- settings/pq_config.json | 2 +- 3 files changed, 333 insertions(+), 89 deletions(-) diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index d53a775..73436af 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -31,6 +31,9 @@ if TYPE_CHECKING: # -------------------------------------------------------------------------- DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100] +DEFAULT_CHESSBOARD_GRID = 5 +INSTANT_PEAK_WINDOW_PERCENTAGE = 10 +INSTANT_PEAK_CAPTURE_DELAY = 0.5 _TEMP_DIR = None _IMAGE_CACHE = {} # {(width, height, percentage): file_path} @@ -89,100 +92,172 @@ def _ensure_window_image(width, height, percentage): return path +def _ensure_solid_image(width, height, rgb, name): + """生成或复用纯色 PNG 文件,返回路径。""" + rgb = tuple(int(v) for v in rgb) + key = ("solid", width, height, rgb) + cached = _IMAGE_CACHE.get(key) + if cached and os.path.exists(cached): + return cached + + arr = np.zeros((height, width, 3), dtype=np.uint8) + arr[:, :] = rgb + path = os.path.join(_get_temp_dir(), f"{name}_{width}x{height}.png") + Image.fromarray(arr, mode="RGB").save(path, format="PNG") + _IMAGE_CACHE[key] = path + return path + + +def _make_checkerboard_image_array(width, height, grid_size, center_white): + """生成棋盘格图像,保证中心块可切换黑/白。""" + image = np.zeros((height, width, 3), dtype=np.uint8) + y_edges = np.linspace(0, height, grid_size + 1, dtype=int) + x_edges = np.linspace(0, width, grid_size + 1, dtype=int) + center_index = grid_size // 2 + + for row in range(grid_size): + for col in range(grid_size): + block_is_white = (row + col) % 2 == 0 + if not center_white: + block_is_white = not block_is_white + value = 255 if block_is_white else 0 + image[ + y_edges[row]:y_edges[row + 1], + x_edges[col]:x_edges[col + 1], + ] = value + + center_value = 255 if center_white else 0 + image[ + y_edges[center_index]:y_edges[center_index + 1], + x_edges[center_index]:x_edges[center_index + 1], + ] = center_value + return image + + +def _ensure_checkerboard_image(width, height, grid_size, center_white): + """生成或复用棋盘格 PNG 文件,返回路径。""" + key = ("checkerboard", width, height, grid_size, center_white) + cached = _IMAGE_CACHE.get(key) + if cached and os.path.exists(cached): + return cached + + arr = _make_checkerboard_image_array(width, height, grid_size, center_white) + center_name = "white_center" if center_white else "black_center" + path = os.path.join( + _get_temp_dir(), + f"checkerboard_{grid_size}x{grid_size}_{center_name}_{width}x{height}.png", + ) + Image.fromarray(arr, mode="RGB").save(path, format="PNG") + _IMAGE_CACHE[key] = path + return path + + +def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"): + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + if isinstance(value, (int, float, np.floating)): + display_value = f"{float(value):.4f}" + else: + display_value = str(value) + + return { + "test_item": test_item, + "pattern": pattern_label, + "value": display_value, + "x": x if isinstance(x, str) else f"{x:.4f}", + "y": y if isinstance(y, str) else f"{y:.4f}", + "time": timestamp, + } + + +def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label): + """读取一次 CA410 数据并包装为表格行。""" + x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() + if lv is None: + raise RuntimeError(f"{pattern_label} 采集失败") + return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv + + +def _send_ld_image(self: "PQAutomationApp", image_path): + self.signal_service.send_image(image_path) + + +def _run_ld_measurement_step(self: "PQAutomationApp", width, height, wait_time, step, log): + label = step["label"] + test_item = step["test_item"] + kind = step["kind"] + + if kind == "window": + percentage = step["percentage"] + image_path = _ensure_window_image(width, height, percentage) + _send_ld_image(self, image_path) + settle_time = wait_time + elif kind == "black": + image_path = _ensure_solid_image(width, height, (0, 0, 0), "black") + _send_ld_image(self, image_path) + settle_time = wait_time + elif kind == "checkerboard": + image_path = _ensure_checkerboard_image( + width, + height, + DEFAULT_CHESSBOARD_GRID, + step["center_white"], + ) + _send_ld_image(self, image_path) + settle_time = wait_time + elif kind == "instant_peak": + black_image = _ensure_solid_image(width, height, (0, 0, 0), "black") + peak_image = _ensure_window_image( + width, + height, + step["percentage"], + ) + _send_ld_image(self, black_image) + log(f" 黑场预置 {wait_time:.1f} 秒", level="info") + time.sleep(wait_time) + _send_ld_image(self, peak_image) + settle_time = min(wait_time, INSTANT_PEAK_CAPTURE_DELAY) + else: + raise ValueError(f"未知 Local Dimming 测试步骤: {kind}") + + log(f" 等待 {settle_time:.1f} 秒后采集...", level="info") + time.sleep(settle_time) + return _measure_ld_row(self, test_item, label) + + +def _set_current_ld_pattern(self: "PQAutomationApp", test_item, pattern_label, percentage=None): + self.current_ld_test_item = test_item + self.current_ld_pattern_label = pattern_label + self.current_ld_percentage = percentage + + # -------------------------------------------------------------------------- # GUI 入口(绑定为 PQAutomationApp 方法) # -------------------------------------------------------------------------- def start_local_dimming_test(self: "PQAutomationApp"): - """开始 Local Dimming 测试。""" - if not self.ca or not self.signal_service.is_connected: - messagebox.showerror("错误", "请先连接 CA410 和 UCD323") - return - - self.ld_start_btn.config(state=tk.DISABLED) - self.ld_stop_btn.config(state=tk.NORMAL) - self.ld_save_btn.config(state=tk.DISABLED) - - for item in self.ld_tree.get_children(): - self.ld_tree.delete(item) - - wait_time = float(self.ld_wait_time_var.get()) - stop_event = threading.Event() - self.ld_stop_event = stop_event - - def worker(): - log = self.log_gui.log - log("=" * 60, level="separator") - log("开始 Local Dimming 测试", level="info") - log("=" * 60, level="separator") - - width, height = self.signal_service.current_resolution() - total = len(DEFAULT_WINDOW_PERCENTAGES) - log(f" 分辨率: {width}x{height}", level="info") - log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}", level="info") - log(f" 等待时间: {wait_time} 秒", level="info") - - results = [] - for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1): - if stop_event.is_set(): - log("测试已停止", level="error") - break - - log(f"[{i}/{total}] 测试 {percentage}% 窗口...", level="info") - try: - image_path = _ensure_window_image(width, height, percentage) - except Exception as e: - log(f" 图像生成失败: {e}", level="error") - continue - - try: - self.signal_service.send_image(image_path) - except Exception as exc: - log(f" {percentage}% 窗口发送失败: {exc},跳过", level="error") - continue - - log(f"等待 {wait_time} 秒...", level="info") - time.sleep(wait_time) - - try: - x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() - except Exception as e: - log(f" 采集亮度异常: {e}", level="error") - continue - if lv is None: - log(f" {percentage}% 窗口采集失败", level="error") - continue - - log(f"采集亮度: {lv:.2f} cd/m²", level="info") - results.append((percentage, x, y, lv, _X, _Y, _Z)) - - log("=" * 60, level="separator") - log(f"Local Dimming 测试完成 ({len(results)}/{total})", level="success") - log("=" * 60, level="separator") - - self.ld_test_results = results - self._dispatch_ui(self.update_ld_results, results) - self._dispatch_ui(self.ld_start_btn.config, state=tk.NORMAL) - self._dispatch_ui(self.ld_stop_btn.config, state=tk.DISABLED) - self._dispatch_ui(self.ld_save_btn.config, state=tk.NORMAL) - - threading.Thread(target=worker, daemon=True).start() + """Local Dimming 不再提供自动测试,保留接口仅提示用户使用手动模式。""" + messagebox.showinfo("提示", "Local Dimming 请使用手动发送图案后再采集亮度") def update_ld_results(self: "PQAutomationApp", results): """把批量测试结果填入 Treeview。""" - for percentage, x, y, lv, _X, _Y, _Z in results: + for row in results: self.ld_tree.insert( "", tk.END, - values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"), + values=( + row["test_item"], + row["pattern"], + row["value"], + row["x"], + row["y"], + row["time"], + ), ) def stop_local_dimming_test(self: "PQAutomationApp"): - """请求停止当前 Local Dimming 测试。""" - ev = getattr(self, "ld_stop_event", None) - if ev: - ev.set() + """兼容旧接口,无操作。""" + return def send_ld_window(self: "PQAutomationApp", percentage): @@ -192,7 +267,7 @@ def send_ld_window(self: "PQAutomationApp", percentage): return self.log_gui.log(f"🔆 发送 {percentage}% 窗口...", level="info") - self.current_ld_percentage = percentage + _set_current_ld_pattern(self, "峰值亮度", f"{percentage}%窗口", percentage) def send(): width, height = self.signal_service.current_resolution() @@ -215,12 +290,120 @@ def send_ld_window(self: "PQAutomationApp", percentage): threading.Thread(target=send, daemon=True).start() +def send_ld_checkerboard(self: "PQAutomationApp", center_white): + """发送棋盘格图案(手动模式)。""" + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + + pattern_label = "棋盘格(中心白)" if center_white else "棋盘格(中心黑)" + self.log_gui.log(f"🔲 发送 {pattern_label}...", level="info") + _set_current_ld_pattern(self, "棋盘格对比度", pattern_label) + + def send(): + width, height = self.signal_service.current_resolution() + try: + image_path = _ensure_checkerboard_image( + width, + height, + DEFAULT_CHESSBOARD_GRID, + center_white, + ) + except Exception as e: + self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}") + return + + try: + self.signal_service.send_image(image_path) + ok = True + except Exception: + ok = False + + msg = f"{pattern_label} 已发送" if ok else f"{pattern_label} 发送失败" + self._dispatch_ui(self.log_gui.log, msg) + + threading.Thread(target=send, daemon=True).start() + + +def send_ld_black_pattern(self: "PQAutomationApp"): + """发送全黑图案(手动模式)。""" + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + + self.log_gui.log("⚫ 发送全黑画面...", level="info") + _set_current_ld_pattern(self, "黑电平", "全黑画面") + + def send(): + width, height = self.signal_service.current_resolution() + try: + image_path = _ensure_solid_image(width, height, (0, 0, 0), "black") + except Exception as e: + self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}") + return + + try: + self.signal_service.send_image(image_path) + ok = True + except Exception: + ok = False + + msg = "全黑画面已发送" if ok else "全黑画面发送失败" + self._dispatch_ui(self.log_gui.log, msg) + + threading.Thread(target=send, daemon=True).start() + + +def send_ld_instant_peak(self: "PQAutomationApp"): + """发送瞬时峰值亮度图案:先黑场,再切到 10% 窗口并保持。""" + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + + pattern_label = f"黑场后切 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口" + self.log_gui.log(f"⚡ 发送瞬时峰值图案: {pattern_label}", level="info") + _set_current_ld_pattern( + self, + "瞬时峰值亮度", + pattern_label, + INSTANT_PEAK_WINDOW_PERCENTAGE, + ) + + def send(): + width, height = self.signal_service.current_resolution() + try: + black_image = _ensure_solid_image(width, height, (0, 0, 0), "black") + peak_image = _ensure_window_image( + width, + height, + INSTANT_PEAK_WINDOW_PERCENTAGE, + ) + except Exception as e: + self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}") + return + + try: + self.signal_service.send_image(black_image) + time.sleep(INSTANT_PEAK_CAPTURE_DELAY) + self.signal_service.send_image(peak_image) + ok = True + except Exception: + ok = False + + msg = ( + f"瞬时峰值图案已发送,当前保持 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口" + if ok else + "瞬时峰值图案发送失败" + ) + self._dispatch_ui(self.log_gui.log, msg) + + def measure_ld_luminance(self: "PQAutomationApp"): """测量当前显示的亮度并追加一行到 Treeview。""" if not self.ca: messagebox.showwarning("警告", "请先连接 CA410 色度计") return - if self.current_ld_percentage is None: + if getattr(self, "current_ld_pattern_label", None) is None: messagebox.showinfo("提示", "请先发送一个窗口图案") return @@ -243,8 +426,12 @@ def measure_ld_luminance(self: "PQAutomationApp"): self._dispatch_ui( self.ld_tree.insert, "", tk.END, values=( - f"{self.current_ld_percentage}%", - f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp, + getattr(self, "current_ld_test_item", "手动采集"), + self.current_ld_pattern_label, + f"{lv:.4f}", + f"{x:.4f}", + f"{y:.4f}", + timestamp, ), ) self._dispatch_ui(self.log_gui.log, f"采集完成: {lv:.2f} cd/m²") @@ -258,6 +445,8 @@ def clear_ld_records(self: "PQAutomationApp"): self.ld_tree.delete(item) self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --") self.current_ld_percentage = None + self.current_ld_test_item = None + self.current_ld_pattern_label = None self.log_gui.log("测试记录已清空", level="info") @@ -282,7 +471,7 @@ def save_local_dimming_results(self: "PQAutomationApp"): try: with open(save_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) - writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"]) + writer.writerow(["测试项目", "图案", "亮度/结果", "x", "y", "时间"]) for item in self.ld_tree.get_children(): writer.writerow(self.ld_tree.item(item, "values")) self.log_gui.log(f"测试结果已保存: {save_path}", level="success") @@ -300,6 +489,9 @@ class LocalDimmingMixin: update_ld_results = update_ld_results stop_local_dimming_test = stop_local_dimming_test send_ld_window = send_ld_window + send_ld_checkerboard = send_ld_checkerboard + send_ld_black_pattern = send_ld_black_pattern + send_ld_instant_peak = send_ld_instant_peak measure_ld_luminance = measure_ld_luminance clear_ld_records = clear_ld_records save_local_dimming_results = save_local_dimming_results diff --git a/app/views/panels/side_panels.py b/app/views/panels/side_panels.py index 804f39c..cfde3a8 100644 --- a/app/views/panels/side_panels.py +++ b/app/views/panels/side_panels.py @@ -29,7 +29,7 @@ def create_log_panel(self: "PQAutomationApp"): def create_local_dimming_panel(self: "PQAutomationApp"): - """创建 Local Dimming 测试面板 - 手动控制版""" + """创建 Local Dimming 测试面板。""" self.local_dimming_frame = ttk.Frame(self.content_frame) # 主容器 @@ -88,6 +88,52 @@ def create_local_dimming_panel(self: "PQAutomationApp"): width=12, ).pack(side=tk.LEFT, padx=3) + # ==================== 3. 其他手动图案 ==================== + pattern_frame = ttk.LabelFrame(main_container, text="🧩 其他测试图案", padding=10) + pattern_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Label( + pattern_frame, + text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度", + font=("", 9), + foreground="#28a745", + ).pack(pady=(0, 8)) + + pattern_row = ttk.Frame(pattern_frame) + pattern_row.pack(fill=tk.X) + + ttk.Button( + pattern_row, + text="棋盘格(中心白)", + command=lambda: self.send_ld_checkerboard(True), + bootstyle="secondary", + width=14, + ).pack(side=tk.LEFT, padx=3) + + ttk.Button( + pattern_row, + text="棋盘格(中心黑)", + command=lambda: self.send_ld_checkerboard(False), + bootstyle="secondary", + width=14, + ).pack(side=tk.LEFT, padx=3) + + ttk.Button( + pattern_row, + text="瞬时峰值", + command=self.send_ld_instant_peak, + bootstyle="warning", + width=12, + ).pack(side=tk.LEFT, padx=3) + + ttk.Button( + pattern_row, + text="全黑画面", + command=self.send_ld_black_pattern, + bootstyle="dark", + width=12, + ).pack(side=tk.LEFT, padx=3) + # ==================== 4. CA410 采集按钮 ==================== measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10) measure_frame.pack(fill=tk.X, pady=(0, 10)) @@ -118,15 +164,19 @@ def create_local_dimming_panel(self: "PQAutomationApp"): result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Treeview - columns = ("窗口百分比", "亮度 (cd/m²)", "x", "y", "时间") + columns = ("测试项目", "图案", "亮度/结果", "x", "y", "时间") self.ld_tree = ttk.Treeview( result_frame, columns=columns, show="headings", height=10 ) for col in columns: self.ld_tree.heading(col, text=col) - if col == "窗口百分比": - self.ld_tree.column(col, width=100, anchor=tk.CENTER) + if col == "测试项目": + self.ld_tree.column(col, width=120, anchor=tk.CENTER) + elif col == "图案": + self.ld_tree.column(col, width=140, anchor=tk.CENTER) + elif col == "亮度/结果": + self.ld_tree.column(col, width=110, anchor=tk.CENTER) elif col == "时间": self.ld_tree.column(col, width=120, anchor=tk.CENTER) else: @@ -176,6 +226,8 @@ def create_local_dimming_panel(self: "PQAutomationApp"): # 初始化当前窗口百分比(用于记录) self.current_ld_percentage = None + self.current_ld_test_item = None + self.current_ld_pattern_label = None def toggle_local_dimming_panel(self: "PQAutomationApp"): diff --git a/settings/pq_config.json b/settings/pq_config.json index 39d311a..3b0c675 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "hdr_movie", + "current_test_type": "screen_module", "test_types": { "screen_module": { "name": "屏模组性能测试",