diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index b04009d..6fa5ec4 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -157,36 +157,6 @@ def _ensure_checkerboard_image(width, height, grid_size, center_white): _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.read_ca_xyLv() - 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 _apply_ld_ucd_params(self: "PQAutomationApp") -> bool: """发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。""" test_type = getattr(self.config, "current_test_type", "screen_module") @@ -315,146 +285,95 @@ def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool: return False -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 +def _send_ld_pattern_async(self: "PQAutomationApp", image_builder, success_msg, fail_msg): + """统一的 Local Dimming 图案发送线程""" -# -------------------------------------------------------------------------- -# GUI 入口(绑定为 PQAutomationApp 方法) -# -------------------------------------------------------------------------- + def worker(): -def start_local_dimming_test(self: "PQAutomationApp"): - """Local Dimming 不再提供自动测试,保留接口仅提示用户使用手动模式。""" - messagebox.showinfo("提示", "Local Dimming 请使用手动发送图案后再采集亮度") - - -def update_ld_results(self: "PQAutomationApp", results): - """把批量测试结果填入 Treeview。""" - for row in results: - self.ld_tree.insert( - "", tk.END, - values=( - row["test_item"], - row["pattern"], - row["value"], - row["x"], - row["y"], - row["time"], - ), - ) - - -def stop_local_dimming_test(self: "PQAutomationApp"): - """兼容旧接口,无操作。""" - return - - -def send_ld_window(self: "PQAutomationApp", percentage): - """发送指定百分比的白色窗口(手动模式)。""" - if not self.signal_service.is_connected: - messagebox.showwarning("警告", "请先连接 UCD323 设备") - return - - try: - luminance_percent = float( - self.ld_window_luminance_var.get() - if hasattr(self, "ld_window_luminance_var") - else 100 - ) - if luminance_percent < 1 or luminance_percent > 100: - raise ValueError("亮度范围应为 1-100") - except Exception as e: - messagebox.showwarning("参数错误", f"窗口亮度参数无效: {e}") - return - - window_level = int(round(luminance_percent / 100.0 * 255.0)) - - self.log_gui.log( - f"🔆 发送 {percentage}% 窗口(亮度{luminance_percent:.0f}%)...", - level="info", - ) - _set_current_ld_pattern( - self, - "峰值亮度", - f"{percentage}%窗口({luminance_percent:.0f}%亮度)", - percentage, - ) - - def send(): if not _apply_ld_ucd_params(self): return + width, height = self.signal_service.current_resolution() + try: - image_path = _ensure_window_image( - width, - height, - percentage, - window_level, - ) + image_path = image_builder(width, height) 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"{percentage}% 窗口({luminance_percent:.0f}%亮度)已发送" if ok - else f"{percentage}% 窗口({luminance_percent:.0f}%亮度)发送失败" - ) + + msg = success_msg if ok else fail_msg self._dispatch_ui(self.log_gui.log, msg) - threading.Thread(target=send, daemon=True).start() + threading.Thread(target=worker, daemon=True).start() + + +# -------------------------------------------------------------------------- +# GUI 入口(绑定为 PQAutomationApp 方法) +# -------------------------------------------------------------------------- +def send_ld_window(self: "PQAutomationApp", percentage): + + FIXED_WINDOW_PERCENTAGE = 40 + + try: + luminance_percent = float(percentage) + if luminance_percent < 1 or luminance_percent > 100: + raise ValueError + except Exception: + messagebox.showwarning("参数错误", "亮度范围应为 1-100") + return + + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + + window_level = int(round(luminance_percent / 100 * 255)) + + self.log_gui.log( + f"发送 {FIXED_WINDOW_PERCENTAGE}%窗口(亮度{luminance_percent:.0f}%)...", + level="info", + ) + + _set_current_ld_pattern( + self, + "峰值亮度", + f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)", + FIXED_WINDOW_PERCENTAGE, + ) + + def builder(width, height): + return _ensure_window_image( + width, + height, + FIXED_WINDOW_PERCENTAGE, + window_level, + ) + + _send_ld_pattern_async( + self, + builder, + f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)已发送", + f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)发送失败", + ) def send_ld_manual_window(self: "PQAutomationApp"): - """按手动输入的窗口百分比和亮度直接发送窗口图案。""" + """按手动输入的窗口大小和亮度发送窗口图案。""" + + if not self.signal_service.is_connected: + messagebox.showwarning("警告", "请先连接 UCD323 设备") + return + try: percentage = int(float(self.ld_window_percentage_var.get())) if percentage < 1 or percentage > 100: @@ -463,133 +382,104 @@ def send_ld_manual_window(self: "PQAutomationApp"): messagebox.showwarning("参数错误", f"窗口百分比无效: {e}") return - self.send_ld_window(percentage) + try: + luminance_percent = float(self.ld_window_luminance_var.get()) + if luminance_percent < 1 or luminance_percent > 100: + raise ValueError("亮度范围应为 1-100") + except Exception as e: + messagebox.showwarning("参数错误", f"窗口亮度无效: {e}") + return + + window_level = int(round(luminance_percent / 100.0 * 255.0)) + + self.log_gui.log( + f"发送 {percentage}%窗口(亮度{luminance_percent:.0f}%)...", + level="info", + ) + + _set_current_ld_pattern( + self, + "峰值亮度", + f"{percentage}%窗口({luminance_percent:.0f}%亮度)", + percentage, + ) + + def builder(width, height): + return _ensure_window_image( + width, + height, + percentage, + window_level, + ) + + _send_ld_pattern_async( + self, + builder, + f"{percentage}%窗口({luminance_percent:.0f}%亮度)已发送", + f"{percentage}%窗口({luminance_percent:.0f}%亮度)发送失败", + ) 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") + + self.log_gui.log(f"发送 {pattern_label}...", level="info") + _set_current_ld_pattern(self, "棋盘格对比度", pattern_label) - def send(): - if not _apply_ld_ucd_params(self): - return - 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 + def builder(width, height): + return _ensure_checkerboard_image( + width, + height, + DEFAULT_CHESSBOARD_GRID, + center_white, + ) - 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() + _send_ld_pattern_async( + self, + builder, + f"{pattern_label} 已发送", + f"{pattern_label} 发送失败", + ) def send_ld_black_pattern(self: "PQAutomationApp"): - """发送全黑图案(手动模式)。""" + if not self.signal_service.is_connected: messagebox.showwarning("警告", "请先连接 UCD323 设备") return - self.log_gui.log("⚫ 发送全黑画面...", level="info") + self.log_gui.log("发送全黑画面...", level="info") + _set_current_ld_pattern(self, "黑电平", "全黑画面") - def send(): - if not _apply_ld_ucd_params(self): - return - 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 + def builder(width, height): + return _ensure_solid_image(width, height, (0, 0, 0), "black") - 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( + _send_ld_pattern_async( self, - "瞬时峰值亮度", - pattern_label, - INSTANT_PEAK_WINDOW_PERCENTAGE, + builder, + "全黑画面已发送", + "全黑画面发送失败", ) - def send(): - if not _apply_ld_ucd_params(self): - return - 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) - - 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 @@ -603,38 +493,72 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"): 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") + + # 无限模式 + no_limit = bool( + self.ld_peak_no_limit_var.get() + if hasattr(self, "ld_peak_no_limit_var") + else False + ) + + if not no_limit: + max_duration = float(self.ld_peak_duration_var.get()) + if max_duration <= 0: + raise ValueError("测量时长必须大于 0") + else: + max_duration = None + + # 回落百分比 + drop_percent = float( + self.ld_peak_drop_percent_var.get() + if hasattr(self, "ld_peak_drop_percent_var") + else 3 + ) + + if drop_percent <= 0 or drop_percent >= 50: + raise ValueError("回落百分比建议 1~50") + 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}%亮度)" + duration_text = "直到亮度回落" if no_limit else f"最长 {max_duration:.1f}s" + self.ld_peak_tracking = True + self.log_gui.log( - f"⚡ 开始独立瞬时峰值测试: {pattern_label},最长 {max_duration:.1f}s", + f"开始瞬时峰值测试: {pattern_label},{duration_text},回落阈值 {drop_percent:.1f}%", level="info", ) - _set_current_ld_pattern(self, "瞬时峰值亮度", pattern_label, window_percentage) + + _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 @@ -645,6 +569,7 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"): 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, @@ -653,75 +578,97 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"): 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: + + # 固定时长模式 + if max_duration is not None: + if elapsed > max_duration: + break + + # 安全保护(30分钟) + if elapsed > 1800: + self._dispatch_ui( + self.log_gui.log, + "安全超时停止(30分钟)", + "warning", + ) break x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() + if lv is None: time.sleep(sample_interval) continue + lv = float(lv) + + # 更新峰值 if peak_lv is None or lv > peak_lv: - peak_lv = float(lv) + peak_lv = lv peak_time = elapsed + # 曲线记录 if record_curve: curve_count += 1 self._dispatch_ui( - self.ld_tree.insert, - "", - tk.END, + self._insert_ld_tree_item, values=( "瞬时峰值曲线", f"{window_percentage}%窗口@{window_luminance_percent:.0f}% t={elapsed:.2f}s", - f"{float(lv):.4f}", + f"{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, - ) + + drop_threshold = peak_lv * (1 - drop_percent / 100.0) + 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" - ), + text=f"亮度:{lv:.2f} cd/m² | 峰值:{(peak_lv or lv):.2f} cd/m² | t:{elapsed:.2f}s", ) + time.sleep(sample_interval) if peak_lv is None: - self._dispatch_ui(self.log_gui.log, "瞬时峰值测试未采到有效亮度", "warning") + 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)) + + sustain_time = max(0.0, end_time - (peak_time or 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(未检测到回落)" + else f"峰值={peak_lv:.2f} cd/m², 持续>{sustain_time:.2f}s" ) self._dispatch_ui( - self.ld_tree.insert, - "", - tk.END, + self._insert_ld_tree_item, values=( "瞬时峰值亮度", pattern_label, @@ -731,30 +678,54 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"): 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") + 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") + 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") + 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 _insert_ld_tree_item(self, parent="", index=tk.END, **kwargs): + item = self.ld_tree.insert(parent, index, **kwargs) + try: + self.ld_tree.see(item) + except Exception: + pass + return item + + def measure_ld_luminance(self: "PQAutomationApp"): """测量当前显示的亮度并追加一行到 Treeview。""" if not self.ca: @@ -764,8 +735,6 @@ def measure_ld_luminance(self: "PQAutomationApp"): messagebox.showinfo("提示", "请先发送一个窗口图案") return - self.log_gui.log("📏 正在采集亮度...", level="info") - def measure(): try: x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() @@ -781,7 +750,7 @@ def measure_ld_luminance(self: "PQAutomationApp"): text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}", ) self._dispatch_ui( - self.ld_tree.insert, "", tk.END, + self._insert_ld_tree_item, values=( getattr(self, "current_ld_test_item", "手动采集"), self.current_ld_pattern_label, @@ -840,48 +809,82 @@ def save_local_dimming_results(self: "PQAutomationApp"): def plot_ld_instant_peak_curve(self: "PQAutomationApp"): - """从测试表格提取瞬时峰值曲线点并生成亮度-时间曲线图。""" - curve_points = [] - pattern = re.compile(r"t\s*=\s*([0-9]+(?:\.[0-9]+)?)s") + """绘制最近一次瞬时峰值测试的亮度-时间曲线""" - for item in self.ld_tree.get_children(): + pattern = re.compile(r"t\s*=\s*([0-9]+(?:\.[0-9]+)?)s") + curve_points = [] + + # 从表格底部向上找最近一次曲线 + items = list(self.ld_tree.get_children())[::-1] + + collecting = False + + for item in items: values = self.ld_tree.item(item, "values") + if len(values) < 3: continue + test_item = str(values[0]) pattern_text = str(values[1]) lv_text = str(values[2]) - if test_item != "瞬时峰值曲线": + + if test_item == "瞬时峰值曲线": + collecting = True + else: + if collecting: + break continue match = pattern.search(pattern_text) if not match: continue + try: t_sec = float(match.group(1)) lv = float(lv_text) except Exception: continue + curve_points.append((t_sec, lv)) if not curve_points: messagebox.showinfo("提示", "没有可绘制的瞬时峰值曲线数据") return + # 时间排序 curve_points.sort(key=lambda x: x[0]) + t_data = [p[0] for p in curve_points] lv_data = [p[1] for p in curve_points] fig = plt.figure(figsize=(8.6, 4.6)) ax = fig.add_subplot(111) - ax.plot(t_data, lv_data, "-o", linewidth=1.8, markersize=3.5, color="#2a9d8f") + + ax.plot( + t_data, + lv_data, + "-o", + linewidth=1.8, + markersize=3.5, + color="#2a9d8f", + ) + ax.set_title("Instant Peak Luminance Curve") ax.set_xlabel("Time (s)") ax.set_ylabel("Luminance (cd/m²)") ax.grid(True, linestyle="--", alpha=0.35) + # 标记峰值 peak_idx = int(np.argmax(lv_data)) - ax.scatter([t_data[peak_idx]], [lv_data[peak_idx]], color="#e76f51", zorder=3) + + ax.scatter( + [t_data[peak_idx]], + [lv_data[peak_idx]], + color="#e76f51", + zorder=3, + ) + ax.annotate( f"Peak: {lv_data[peak_idx]:.2f} cd/m² @ {t_data[peak_idx]:.2f}s", (t_data[peak_idx], lv_data[peak_idx]), @@ -893,24 +896,23 @@ def plot_ld_instant_peak_curve(self: "PQAutomationApp"): fig.tight_layout() plt.show(block=False) - self.log_gui.log("已生成瞬时峰值曲线图", level="success") + + self.log_gui.log("已生成本次瞬时峰值曲线图", level="success") class LocalDimmingMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ - start_local_dimming_test = start_local_dimming_test - update_ld_results = update_ld_results - stop_local_dimming_test = stop_local_dimming_test send_ld_window = send_ld_window send_ld_manual_window = send_ld_manual_window 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 plot_ld_instant_peak_curve = plot_ld_instant_peak_curve + + _insert_ld_tree_item = _insert_ld_tree_item diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index 9953fa2..525cd1e 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -774,7 +774,6 @@ def create_test_type_frame(self: "PQAutomationApp"): ).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w") panel_buttons = [ - ("log_btn", "测试日志", self.toggle_log_panel), ("ai_image_btn", "AI 图片", self.toggle_ai_image_panel), ("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel), ("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel), @@ -801,6 +800,17 @@ def create_test_type_frame(self: "PQAutomationApp"): ) beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4)) + + # ---------- 测试日志(底部固定) ---------- + self.log_btn = ttk.Button( + self.sidebar_frame, + text="测试日志", + style="Sidebar.TButton", + command=self.toggle_log_panel, + takefocus=False, + ) + self.log_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM) + # ---------- 主题切换(底部固定) ---------- self.theme_toggle_btn = ttk.Button( self.sidebar_frame, @@ -814,8 +824,6 @@ def create_test_type_frame(self: "PQAutomationApp"): # 注册面板按钮 if hasattr(self, "panels"): - if "log" in self.panels: - self.panels["log"]["button"] = self.log_btn if "ai_image" in self.panels: self.panels["ai_image"]["button"] = self.ai_image_btn if "single_step" in self.panels: @@ -1020,10 +1028,10 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None): # 根据分辨率给出提示 if width >= 3840: # 4K及以上 - self.log_gui.log(" ℹ️ 检测到4K分辨率", level="info") + self.log_gui.log("检测到4K分辨率", level="info") if refresh_rate >= 120: - self.log_gui.log(" ℹ️ 检测到高刷新率", level="info") + self.log_gui.log("检测到高刷新率", level="info") # 更新屏模组配置(独立于 current_test_type) self.config.current_test_types.setdefault("screen_module", {})[ diff --git a/app/views/panels/side_panels.py b/app/views/panels/side_panels.py index 71b7a97..4409004 100644 --- a/app/views/panels/side_panels.py +++ b/app/views/panels/side_panels.py @@ -42,13 +42,13 @@ def create_local_dimming_panel(self: "PQAutomationApp"): ttk.Label( title_frame, - text="🔆 Local Dimming 窗口测试", + text="Local Dimming 窗口测试", font=("微软雅黑", 14, "bold"), ).pack(side=tk.LEFT) # ==================== 2. 窗口百分比按钮 ==================== window_frame = ttk.LabelFrame( - main_container, text="🔆 窗口百分比(点击发送)", padding=10 + main_container, text="窗口百分比", padding=10 ) window_frame.pack(fill=tk.X, pady=(0, 10)) @@ -133,16 +133,9 @@ def create_local_dimming_panel(self: "PQAutomationApp"): ).pack(side=tk.LEFT, padx=3) # ==================== 3. 其他手动图案 ==================== - pattern_frame = ttk.LabelFrame(main_container, text="🧩 其他测试图案", padding=10) + 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), - style="SuccessState.TLabel", - ).pack(pady=(0, 8)) - pattern_row = ttk.Frame(pattern_frame) pattern_row.pack(fill=tk.X) @@ -171,21 +164,16 @@ def create_local_dimming_panel(self: "PQAutomationApp"): ).pack(side=tk.LEFT, padx=3) # ==================== 4. 独立瞬时峰值连续测试 ==================== - peak_frame = ttk.LabelFrame(main_container, text="⚡ 瞬时峰值独立测试", padding=10) + 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) + self.ld_peak_no_limit_var = tk.BooleanVar(value=False) + self.ld_peak_drop_percent_var = tk.StringVar(value="3") 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( @@ -242,8 +230,25 @@ def create_local_dimming_panel(self: "PQAutomationApp"): ) self.ld_peak_stop_btn.pack(side=tk.LEFT) + ttk.Label(peak_frame, text="亮度回落(%):").grid( + row=2, column=0, sticky=tk.W, padx=(0, 4), pady=(6, 0) + ) + + ttk.Entry( + peak_frame, + textvariable=self.ld_peak_drop_percent_var, + width=8 + ).grid(row=2, column=1, sticky=tk.W, pady=(6, 0)) + + ttk.Checkbutton( + peak_frame, + text="不固定测试时间", + variable=self.ld_peak_no_limit_var, + bootstyle="round-toggle", + ).grid(row=2, column=2, columnspan=3, sticky=tk.W, pady=(6, 0)) + # ==================== 5. CA410 采集按钮 ==================== - measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10) + measure_frame = ttk.LabelFrame(main_container, text="CA410 测量", padding=10) measure_frame.pack(fill=tk.X, pady=(0, 10)) measure_btn_frame = ttk.Frame(measure_frame) @@ -251,7 +256,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): self.ld_measure_btn = ttk.Button( measure_btn_frame, - text="📏 采集当前亮度", + text="采集当前亮度", command=self.measure_ld_luminance, bootstyle="primary", width=15, @@ -268,7 +273,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0)) # ==================== 6. 测试结果表格 ==================== - result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10) + result_frame = ttk.LabelFrame(main_container, text="测试记录", padding=10) result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # Treeview @@ -314,7 +319,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): self.ld_save_btn = ttk.Button( bottom_frame, - text="💾 保存结果", + text="保存结果", command=self.save_local_dimming_results, bootstyle="info", width=12, @@ -323,7 +328,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"): self.ld_plot_btn = ttk.Button( bottom_frame, - text="📈 生成峰值曲线", + text="生成峰值曲线", command=self.plot_ld_instant_peak_curve, bootstyle="warning-outline", width=14, diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 2b10e97..33380b1 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -393,7 +393,7 @@ class PQAutomationApp( if not hasattr(self, "_set_custom_template_tab_visible"): return - # 客户模板结果 Tab 只属于 SDR Movie。 + # 客户模板结果 Tab 只属于 SDR Movie if test_type != "sdr_movie": self._set_custom_template_tab_visible(False) return @@ -416,14 +416,9 @@ class PQAutomationApp( def change_test_type(self, test_type): """切换测试类型""" - # 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板 + # 切换测试类型时,自动隐藏日志面板 if self.current_panel in ( "log", - "local_dimming", - "ai_image", - "single_step", - "pantone_baseline", - "gamma_pattern", ): self.hide_all_panels() self._save_cct_params_before_test_type_switch() diff --git a/settings/pq_config.json b/settings/pq_config.json index 73fceaf..e72c6bc 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "sdr_movie", + "current_test_type": "local_dimming", "test_types": { "screen_module": { "name": "屏模组性能测试", diff --git a/tools/demo_accuracy_plot.py b/tools/demo_accuracy_plot.py deleted file mode 100644 index cdb106d..0000000 --- a/tools/demo_accuracy_plot.py +++ /dev/null @@ -1,188 +0,0 @@ -"""离线色准图 Demo。 - -运行后会在 tools/demo_outputs/ 下生成一张 PNG, -用于在没有 UCD 设备时预览当前色准图表的 Calman 风格布局。 -""" - -from __future__ import annotations - -import argparse -import math -import sys -from pathlib import Path - -import matplotlib - -matplotlib.use("Agg") - -import matplotlib.pyplot as plt -import numpy as np - -plt.rcParams["font.family"] = ["sans-serif"] -plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"] -plt.rcParams["axes.unicode_minus"] = False - -REPO_ROOT = Path(__file__).resolve().parents[1] -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) - -from app.plots.plot_accuracy import plot_accuracy -from app.tests.color_accuracy import ( - calculate_delta_e_2000, - get_accuracy_color_standards, -) - - -COLOR_NAMES = [ - "White", - "Gray 80", - "Gray 65", - "Gray 50", - "Gray 35", - "Dark Skin", - "Light Skin", - "Blue Sky", - "Foliage", - "Blue Flower", - "Bluish Green", - "Orange", - "Purplish Blue", - "Moderate Red", - "Purple", - "Yellow Green", - "Orange Yellow", - "Blue (Legacy)", - "Green (Legacy)", - "Red (Legacy)", - "Yellow (Legacy)", - "Magenta (Legacy)", - "Cyan (Legacy)", - "100% Red", - "100% Green", - "100% Blue", - "100% Cyan", - "100% Magenta", - "100% Yellow", -] - - -class _DummyNotebook: - def select(self, *_args, **_kwargs): - return None - - -class _DummyCanvas: - def draw(self): - return None - - -class _DemoApp: - def __init__(self, fig): - self.accuracy_fig = fig - self.accuracy_canvas = _DummyCanvas() - self.chart_notebook = _DummyNotebook() - self.accuracy_chart_frame = object() - - def get_test_type_name(self, test_type): - mapping = { - "sdr_movie": "SDR Movie", - "hdr_movie": "HDR Movie", - "screen_module": "屏模组", - } - return mapping.get(test_type, str(test_type)) - - -def _build_demo_data(test_type: str = "sdr_movie"): - standards = get_accuracy_color_standards(test_type) - rng = np.random.default_rng(20260527) - - measured = [] - color_patches = [] - delta_e_values = [] - - for idx, name in enumerate(COLOR_NAMES): - sx, sy = standards[name] - - # 构造一些“看起来像真实测量”的偏移: - # 大部分点轻微偏移,少数点更明显,便于看出方向和等级差异。 - if idx < 5: - offset_scale = 0.0012 - elif idx < 23: - offset_scale = 0.0028 - else: - offset_scale = 0.0045 - - angle = rng.uniform(0, 2 * math.pi) - radius = offset_scale * (0.55 + 0.85 * rng.random()) - dx = math.cos(angle) * radius - dy = math.sin(angle) * radius - - # 为了让图上连线不完全随机,给部分饱和色再加一点定向偏移。 - if idx >= 23: - dx += 0.002 * (1 if idx % 2 == 0 else -1) - dy += 0.0015 * (1 if idx % 3 == 0 else -1) - - mx = min(max(sx + dx, 0.0), 0.8) - my = min(max(sy + dy, 0.0), 0.9) - - # 亮度也做一点微小变化,避免所有点完全同一层。 - measured_lv = 70.0 + rng.normal(0, 4.0) - measured_lv = max(measured_lv, 1.0) - - delta_e = calculate_delta_e_2000(mx, my, measured_lv, sx, sy) - - measured.append((mx, my, measured_lv)) - color_patches.append(name) - delta_e_values.append(delta_e) - - avg_delta_e = float(np.mean(delta_e_values)) - max_delta_e = float(np.max(delta_e_values)) - min_delta_e = float(np.min(delta_e_values)) - - return { - "color_patches": color_patches, - "delta_e_values": delta_e_values, - "color_measurements": measured, - "avg_delta_e": avg_delta_e, - "max_delta_e": max_delta_e, - "min_delta_e": min_delta_e, - "excellent_count": sum(1 for value in delta_e_values if value < 3), - "good_count": sum(1 for value in delta_e_values if 3 <= value < 5), - "poor_count": sum(1 for value in delta_e_values if value >= 5), - "avg_delta_e_gray": float(np.mean(delta_e_values[0:5])), - "avg_delta_e_colorchecker": float(np.mean(delta_e_values[5:23])), - "avg_delta_e_saturated": float(np.mean(delta_e_values[23:29])), - "target_gamma": 2.2, - } - - -def main(): - parser = argparse.ArgumentParser(description="Generate an offline color accuracy demo PNG.") - parser.add_argument( - "--output", - type=Path, - default=Path(__file__).resolve().parent / "demo_outputs" / "accuracy_demo.png", - help="Output PNG path.", - ) - parser.add_argument( - "--test-type", - choices=["sdr_movie", "hdr_movie", "screen_module"], - default="sdr_movie", - help="Test type used for the title and standard color set.", - ) - args = parser.parse_args() - - args.output.parent.mkdir(parents=True, exist_ok=True) - - fig = plt.Figure(figsize=(14, 8), dpi=120, tight_layout=False) - app = _DemoApp(fig) - accuracy_data = _build_demo_data(args.test_type) - - plot_accuracy(app, accuracy_data, args.test_type) - fig.savefig(args.output, dpi=220) - - print(f"Saved demo image to: {args.output}") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tools/demo_outputs/accuracy_demo.png b/tools/demo_outputs/accuracy_demo.png deleted file mode 100644 index 298d370..0000000 Binary files a/tools/demo_outputs/accuracy_demo.png and /dev/null differ