diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index eabd072..2f0bf9b 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -34,6 +34,9 @@ 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 +INSTANT_PEAK_DROP_RATIO = 0.97 +INSTANT_PEAK_MIN_DROP_NITS = 2.0 +INSTANT_PEAK_SAMPLE_INTERVAL = 0.3 _TEMP_DIR = None _IMAGE_CACHE = {} # {(width, height, percentage): file_path} @@ -63,8 +66,8 @@ def _get_temp_dir(): return _TEMP_DIR -def _make_window_image_array(width, height, percentage): - """生成黑底+居中白窗的 numpy 图像,保持屏幕比例。""" +def _make_window_image_array(width, height, percentage, window_level=255): + """生成黑底+居中窗口图像,保持屏幕比例。""" image = np.zeros((height, width, 3), dtype=np.uint8) if percentage >= 100: ww, wh = width, height @@ -74,18 +77,19 @@ def _make_window_image_array(width, height, percentage): wh = int(height * scale) x1 = (width - ww) // 2 y1 = (height - wh) // 2 - image[y1:y1 + wh, x1:x1 + ww] = 255 + image[y1:y1 + wh, x1:x1 + ww] = int(window_level) return image -def _ensure_window_image(width, height, percentage): +def _ensure_window_image(width, height, percentage, window_level=255): """生成或复用缓存的窗口 PNG 文件,返回路径。""" - key = (width, height, percentage) + level = max(0, min(255, int(window_level))) + key = (width, height, percentage, level) cached = _IMAGE_CACHE.get(key) if cached and os.path.exists(cached): return cached - arr = _make_window_image_array(width, height, percentage) - fname = f"window_{width}x{height}_{percentage:03d}percent.png" + arr = _make_window_image_array(width, height, percentage, level) + fname = f"window_{width}x{height}_{percentage:03d}percent_{level:03d}lv.png" path = os.path.join(_get_temp_dir(), fname) Image.fromarray(arr, mode="RGB").save(path, format="PNG") _IMAGE_CACHE[key] = path @@ -536,6 +540,179 @@ def send_ld_instant_peak(self: "PQAutomationApp"): threading.Thread(target=send, daemon=True).start() +def start_ld_instant_peak_tracking(self: "PQAutomationApp"): + """独立瞬时峰值测试:持续采样直到亮度回落或达到最长测量时长。""" + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + if not self.ca: + messagebox.showwarning("警告", "请先连接 CA410 色度计") + return + if getattr(self, "ld_peak_tracking", False): + messagebox.showinfo("提示", "瞬时峰值测试正在进行中") + return + + try: + window_percentage = int(float(self.ld_peak_window_size_var.get())) + if window_percentage < 1 or window_percentage > 100: + raise ValueError("窗口百分比超出范围") + + window_luminance_percent = float(self.ld_peak_window_luminance_var.get()) + if window_luminance_percent < 1 or window_luminance_percent > 100: + raise ValueError("窗口亮度超出范围") + + max_duration = float(self.ld_peak_duration_var.get()) + if max_duration <= 0: + raise ValueError("测量时长必须大于 0") + + sample_interval = float( + self.ld_peak_sample_interval_var.get() + if hasattr(self, "ld_peak_sample_interval_var") + else INSTANT_PEAK_SAMPLE_INTERVAL + ) + if sample_interval <= 0: + raise ValueError("采样间隔必须大于 0") + except Exception as e: + messagebox.showwarning("参数错误", f"请检查瞬时峰值参数: {e}") + return + + record_curve = bool(self.ld_peak_record_curve_var.get()) + window_level = int(round(window_luminance_percent / 100.0 * 255.0)) + pattern_label = f"黑场后切 {window_percentage}%窗口({window_luminance_percent:.0f}%亮度)" + + self.ld_peak_tracking = True + self.log_gui.log( + f"⚡ 开始独立瞬时峰值测试: {pattern_label},最长 {max_duration:.1f}s", + level="info", + ) + _set_current_ld_pattern(self, "瞬时峰值亮度", pattern_label, window_percentage) + + if hasattr(self, "ld_peak_start_btn"): + self.ld_peak_start_btn.configure(state="disabled") + if hasattr(self, "ld_peak_stop_btn"): + self.ld_peak_stop_btn.configure(state="normal") + + def run(): + peak_lv = None + peak_time = None + drop_time = None + curve_count = 0 + + try: + if not _apply_ld_ucd_params(self): + return + + width, height = self.signal_service.current_resolution() + black_image = _ensure_solid_image(width, height, (0, 0, 0), "black") + peak_image = _ensure_window_image( + width, + height, + window_percentage, + window_level, + ) + + self.signal_service.send_image(black_image) + time.sleep(INSTANT_PEAK_CAPTURE_DELAY) + self.signal_service.send_image(peak_image) + + started = time.time() + while self.ld_peak_tracking: + elapsed = time.time() - started + if elapsed > max_duration: + break + + x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() + if lv is None: + time.sleep(sample_interval) + continue + + if peak_lv is None or lv > peak_lv: + peak_lv = float(lv) + peak_time = elapsed + + if record_curve: + curve_count += 1 + self._dispatch_ui( + self.ld_tree.insert, + "", + tk.END, + values=( + "瞬时峰值曲线", + f"{window_percentage}%窗口@{window_luminance_percent:.0f}% t={elapsed:.2f}s", + f"{float(lv):.4f}", + f"{x:.4f}", + f"{y:.4f}", + datetime.datetime.now().strftime("%H:%M:%S"), + ), + ) + + if peak_lv is not None: + drop_threshold = max( + peak_lv * INSTANT_PEAK_DROP_RATIO, + peak_lv - INSTANT_PEAK_MIN_DROP_NITS, + ) + if lv < drop_threshold and elapsed > (peak_time or 0): + drop_time = elapsed + break + + self._dispatch_ui( + self.ld_result_label.config, + text=( + f"亮度: {lv:.2f} cd/m² | 峰值: {(peak_lv or lv):.2f} cd/m²" + f" | t: {elapsed:.2f}s" + ), + ) + time.sleep(sample_interval) + + if peak_lv is None: + self._dispatch_ui(self.log_gui.log, "瞬时峰值测试未采到有效亮度", "warning") + return + + end_time = drop_time if drop_time is not None else (time.time() - started) + sustain_time = max(0.0, end_time - (peak_time or 0.0)) + result_label = ( + f"峰值={peak_lv:.2f} cd/m², 持续={sustain_time:.2f}s" + if drop_time is not None + else f"峰值={peak_lv:.2f} cd/m², 持续>{sustain_time:.2f}s(未检测到回落)" + ) + + self._dispatch_ui( + self.ld_tree.insert, + "", + tk.END, + values=( + "瞬时峰值亮度", + pattern_label, + result_label, + "--", + "--", + datetime.datetime.now().strftime("%H:%M:%S"), + ), + ) + self._dispatch_ui( + self.log_gui.log, + f"瞬时峰值测试完成: {result_label},曲线点 {curve_count} 个", + "success", + ) + except Exception as e: + self._dispatch_ui(self.log_gui.log, f"瞬时峰值测试异常: {e}", "error") + finally: + self.ld_peak_tracking = False + if hasattr(self, "ld_peak_start_btn"): + self._dispatch_ui(self.ld_peak_start_btn.configure, state="normal") + if hasattr(self, "ld_peak_stop_btn"): + self._dispatch_ui(self.ld_peak_stop_btn.configure, state="disabled") + + threading.Thread(target=run, daemon=True).start() + + +def stop_ld_instant_peak_tracking(self: "PQAutomationApp"): + """停止独立瞬时峰值连续采样。""" + if getattr(self, "ld_peak_tracking", False): + self.ld_peak_tracking = False + self.log_gui.log("已请求停止瞬时峰值测试", level="info") + + def measure_ld_luminance(self: "PQAutomationApp"): """测量当前显示的亮度并追加一行到 Treeview。""" if not self.ca: @@ -585,6 +762,7 @@ def clear_ld_records(self: "PQAutomationApp"): self.current_ld_percentage = None self.current_ld_test_item = None self.current_ld_pattern_label = None + self.ld_peak_tracking = False self.log_gui.log("测试记录已清空", level="info") @@ -630,6 +808,8 @@ class LocalDimmingMixin: send_ld_checkerboard = send_ld_checkerboard send_ld_black_pattern = send_ld_black_pattern send_ld_instant_peak = send_ld_instant_peak + start_ld_instant_peak_tracking = start_ld_instant_peak_tracking + stop_ld_instant_peak_tracking = stop_ld_instant_peak_tracking 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 034bb38..4055751 100644 --- a/app/views/panels/side_panels.py +++ b/app/views/panels/side_panels.py @@ -134,7 +134,79 @@ def create_local_dimming_panel(self: "PQAutomationApp"): width=12, ).pack(side=tk.LEFT, padx=3) - # ==================== 4. CA410 采集按钮 ==================== + # ==================== 4. 独立瞬时峰值连续测试 ==================== + peak_frame = ttk.LabelFrame(main_container, text="⚡ 瞬时峰值独立测试", padding=10) + peak_frame.pack(fill=tk.X, pady=(0, 10)) + + ttk.Label( + peak_frame, + text="先黑场切窗口后连续测亮度,直到回落或到达最长测量时间", + font=("", 9), + style="WarningState.TLabel", + ).grid(row=0, column=0, columnspan=8, sticky=tk.W, pady=(0, 8)) + + self.ld_peak_window_size_var = tk.StringVar(value="10") + self.ld_peak_window_luminance_var = tk.StringVar(value="100") + self.ld_peak_duration_var = tk.StringVar(value="20") + self.ld_peak_sample_interval_var = tk.StringVar(value="0.3") + self.ld_peak_record_curve_var = tk.BooleanVar(value=True) + + ttk.Label(peak_frame, text="窗口(%):").grid(row=1, column=0, sticky=tk.W, padx=(0, 4)) + ttk.Entry(peak_frame, textvariable=self.ld_peak_window_size_var, width=8).grid( + row=1, column=1, sticky=tk.W, padx=(0, 10) + ) + + ttk.Label(peak_frame, text="窗口亮度(%):").grid( + row=1, column=2, sticky=tk.W, padx=(0, 4) + ) + ttk.Entry(peak_frame, textvariable=self.ld_peak_window_luminance_var, width=8).grid( + row=1, column=3, sticky=tk.W, padx=(0, 10) + ) + + ttk.Label(peak_frame, text="连续时长(s):").grid( + row=1, column=4, sticky=tk.W, padx=(0, 4) + ) + ttk.Entry(peak_frame, textvariable=self.ld_peak_duration_var, width=8).grid( + row=1, column=5, sticky=tk.W, padx=(0, 10) + ) + + ttk.Label(peak_frame, text="采样间隔(s):").grid( + row=1, column=6, sticky=tk.W, padx=(0, 4) + ) + ttk.Entry(peak_frame, textvariable=self.ld_peak_sample_interval_var, width=8).grid( + row=1, column=7, sticky=tk.W + ) + + ttk.Checkbutton( + peak_frame, + text="记录曲线点到表格", + variable=self.ld_peak_record_curve_var, + bootstyle="round-toggle", + ).grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(8, 0)) + + peak_btn_row = ttk.Frame(peak_frame) + peak_btn_row.grid(row=2, column=4, columnspan=4, sticky=tk.EW, pady=(8, 0)) + + self.ld_peak_start_btn = ttk.Button( + peak_btn_row, + text="开始峰值追踪", + command=self.start_ld_instant_peak_tracking, + bootstyle="warning", + width=14, + ) + self.ld_peak_start_btn.pack(side=tk.LEFT, padx=(0, 5)) + + self.ld_peak_stop_btn = ttk.Button( + peak_btn_row, + text="停止", + command=self.stop_ld_instant_peak_tracking, + bootstyle="danger-outline", + width=10, + state="disabled", + ) + self.ld_peak_stop_btn.pack(side=tk.LEFT) + + # ==================== 5. CA410 采集按钮 ==================== measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10) measure_frame.pack(fill=tk.X, pady=(0, 10)) @@ -159,7 +231,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): ) self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0)) - # ==================== 5. 测试结果表格 ==================== + # ==================== 6. 测试结果表格 ==================== result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10) result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) @@ -191,7 +263,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.ld_tree.configure(yscrollcommand=scrollbar.set) - # ==================== 6. 底部操作按钮 ==================== + # ==================== 7. 底部操作按钮 ==================== bottom_frame = ttk.Frame(main_container) bottom_frame.pack(fill=tk.X) @@ -228,6 +300,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): self.current_ld_percentage = None self.current_ld_test_item = None self.current_ld_pattern_label = None + self.ld_peak_tracking = False def toggle_local_dimming_panel(self: "PQAutomationApp"): diff --git a/settings/pq_config.json b/settings/pq_config.json index c03d6d4..f7ba9fa 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "local_dimming", + "current_test_type": "screen_module", "test_types": { "screen_module": { "name": "屏模组性能测试",