From c6a2e4ada066e605bed8016361e49043caf387c0 Mon Sep 17 00:00:00 2001 From: sladro Date: Mon, 1 Dec 2025 13:35:08 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=9B=91=E6=8E=A7UI?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=80=80=E5=87=BA=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 241 ++++++++++++++++++++++++++++++++++++++++++++-- src/ui/tech_ui.py | 171 ++++++++++++++++++++++++-------- 2 files changed, 363 insertions(+), 49 deletions(-) diff --git a/main.py b/main.py index 2433b6f..ebc6c95 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ import argparse import sys import os from pathlib import Path +from typing import Optional # 抑制OpenCV日志输出 os.environ['OPENCV_LOG_LEVEL'] = 'ERROR' @@ -22,9 +23,13 @@ else: sys.path.insert(0, str(project_root)) +from src.utils.path_utils import resolve_app_path +from src.utils.performance_limiter import PerformanceLimiter + from src.camera.opencv_camera import OpenCVCamera from src.preprocessing.image_enhancer import ImageEnhancer from src.roi_detection.led_detector import LEDDetector +from src.roi_detection.threshold_detector import LEDState from src.output.result_formatter import ResultFormatter from src.output.logger import LEDLogger from src.output.alarm_manager import AlarmManager @@ -37,14 +42,31 @@ class YantaiVisionXSystem: 集成所有模块实现完整的LED检测流水线 """ - def __init__(self, config_dir: str = "config"): + def __init__( + self, + config_dir: str = "config", + roi_config_path: Optional[str] = None, + algorithm_config_path: Optional[str] = None, + ): """ 初始化系统 Args: config_dir: 配置文件目录 """ - self.config_dir = Path(config_dir) + self.config_dir = resolve_app_path(config_dir) + self.config_dir.mkdir(parents=True, exist_ok=True) + + self._roi_config_path = ( + resolve_app_path(roi_config_path) + if roi_config_path is not None + else self.config_dir / "roi_config.yaml" + ) + self._algorithm_config_path = ( + resolve_app_path(algorithm_config_path) + if algorithm_config_path is not None + else self.config_dir / "algorithm_config.yaml" + ) # 初始化各个组件 self.camera = None @@ -54,11 +76,66 @@ class YantaiVisionXSystem: self.logger = LEDLogger() self.alarm_manager = AlarmManager(self.logger) self.tech_ui = TechUI() + self.performance_limiter = PerformanceLimiter() + + self._window_internal_name = "YantaiVisionX Monitor" + self._window_display_name = "烟台蓬莱国际机场低能见度识别软件" + self._window_created = False + self._default_runtime_message = "运行结果:实时检测中" # 状态变量 self.is_running = False self.display_enabled = False + self.runtime_message = self._default_runtime_message + def set_config_paths( + self, + roi_config_path: Optional[str] = None, + algorithm_config_path: Optional[str] = None, + ) -> None: + """动态更新配置文件路径""" + + if roi_config_path is not None: + self._roi_config_path = resolve_app_path(roi_config_path) + + if algorithm_config_path is not None: + self._algorithm_config_path = resolve_app_path(algorithm_config_path) + + def get_config_paths(self) -> dict: + """获取当前配置文件路径""" + + return { + "roi": str(self._roi_config_path), + "algorithm": str(self._algorithm_config_path), + } + + def _configure_performance_limiter(self) -> None: + """根据算法配置更新性能限制。""" + + performance_cfg = {} + if self.led_detector and hasattr(self.led_detector, "algorithm_config"): + performance_cfg = self.led_detector.algorithm_config.get("performance", {}) or {} + + max_fps = performance_cfg.get("max_processing_fps") + frame_stride = performance_cfg.get("frame_stride", 1) + self.performance_limiter.update_settings(max_processing_fps=max_fps, frame_stride=frame_stride) + self.logger.log_info( + f"性能限制器配置: max_fps={self.performance_limiter.max_processing_fps}, " + f"frame_stride={self.performance_limiter.frame_stride}" + ) + + def update_performance_settings( + self, + max_processing_fps: Optional[float] = None, + frame_stride: Optional[int] = None, + ) -> None: + """运行中动态调整性能限制参数""" + + self.performance_limiter.update_settings( + max_processing_fps=max_processing_fps, + frame_stride=frame_stride, + ) + def initialize_system(self, camera_config=None) -> bool: """ 初始化整个系统 @@ -77,15 +154,17 @@ class YantaiVisionXSystem: self.logger.log_info("图像增强器初始化完成") # 2. 初始化LED检测器 - roi_config_path = self.config_dir / "roi_config.yaml" - algorithm_config_path = self.config_dir / "algorithm_config.yaml" + roi_config_path = self._roi_config_path + algorithm_config_path = self._algorithm_config_path self.led_detector = LEDDetector( - str(roi_config_path), - str(algorithm_config_path) + roi_config_path, + algorithm_config_path ) self.logger.log_info("LED检测器初始化完成") + self._configure_performance_limiter() + if self.led_detector and hasattr(self.led_detector, 'algorithm_config'): self.alarm_manager.update_config( self.led_detector.algorithm_config.get('alarm') @@ -124,6 +203,7 @@ class YantaiVisionXSystem: self.display_enabled = display self.is_running = True + self._reset_runtime_message() # 打开摄像头 if not self.camera.open(): @@ -132,6 +212,9 @@ class YantaiVisionXSystem: self.logger.log_info("开始 LED 检测...") + limiter = self.performance_limiter + frame_index = 0 + try: while self.is_running: # 读取帧 @@ -139,6 +222,11 @@ class YantaiVisionXSystem: if not success: self.logger.log_warning("读取帧失败") continue + + current_frame_index = frame_index + frame_index += 1 + if limiter and not limiter.should_process_frame(current_frame_index): + continue # 图像预处理 enhanced_frame = self.image_enhancer.preprocess_frame( @@ -173,6 +261,9 @@ class YantaiVisionXSystem: if self.display_enabled: self._display_results(frame, detection_result) + if limiter: + limiter.enforce_rate_limit() + # 检查退出条件 if cv2.waitKey(1) & 0xFF == ord('q'): break @@ -210,20 +301,33 @@ class YantaiVisionXSystem: self.display_enabled = display self.is_running = True + self._reset_runtime_message() # 创建输出目录 output_path = Path(output_dir) output_path.mkdir(exist_ok=True) results = [] if self.logger.save_results_enabled else None + last_frame = None + last_detection_result = None self.logger.log_info(f"开始处理视频: {video_path}") + limiter = self.performance_limiter + frame_index = 0 + try: while self.is_running: success, frame = self.camera.read_frame() if not success: + if self.display_enabled and last_frame is not None: + self._freeze_last_frame(last_frame, last_detection_result) break # 视频结束 + + current_frame_index = frame_index + frame_index += 1 + if limiter and not limiter.should_process_frame(current_frame_index): + continue # 图像预处理 enhanced_frame = self.image_enhancer.preprocess_frame( @@ -234,6 +338,11 @@ class YantaiVisionXSystem: detection_result = self.led_detector.detect_leds(enhanced_frame) if results is not None: results.append(detection_result) + if self.display_enabled and frame is not None: + last_frame = frame.copy() + else: + last_frame = None + last_detection_result = detection_result # 记录结果 if detection_result.frame_count % 100 == 0: @@ -254,6 +363,9 @@ class YantaiVisionXSystem: self._display_results(frame, detection_result) if cv2.waitKey(1) & 0xFF == ord('q'): break + + if limiter: + limiter.enforce_rate_limit() # 保存批量结果 if results: @@ -290,6 +402,9 @@ class YantaiVisionXSystem: return True + def _reset_runtime_message(self) -> None: + self.runtime_message = self._default_runtime_message + def _display_results(self, original_frame, detection_result) -> None: """ 显示检测结果 @@ -304,10 +419,15 @@ class YantaiVisionXSystem: ) # 使用科技感UI创建完整显示帧 - display_frame = self.tech_ui.create_display_frame(vis_frame, detection_result) + display_frame = self.tech_ui.create_display_frame( + vis_frame, + detection_result, + runtime_message=self.runtime_message, + ) + self._ensure_window() # 显示图像 - cv2.imshow('烟台蓬莱国际机场低能见度识别软件', display_frame) + cv2.imshow(self._window_internal_name, display_frame) # 在控制台显示矩阵状态(每10帧显示一次) if detection_result.frame_count % 10 == 0: @@ -315,6 +435,63 @@ class YantaiVisionXSystem: self.formatter.format_to_matrix(detection_result) ) print(f"\n{matrix_text}") + + def _freeze_last_frame(self, frame, detection_result) -> None: + """视频播放结束后保持最后一帧并显示运行结果""" + self.runtime_message = self._build_runtime_summary(detection_result) + + if not self.display_enabled or frame is None: + return + + try: + self._display_results(frame, detection_result) + except Exception as exc: + self.logger.log_warning(f"最后一帧渲染失败: {exc}") + return + + self.logger.log_info("视频播放结束,按Q或Esc退出最终画面") + while True: + key = cv2.waitKey(100) & 0xFF + if key in (ord('q'), 27): + break + try: + visible = cv2.getWindowProperty(self._window_internal_name, cv2.WND_PROP_VISIBLE) + except Exception: + visible = -1 + if visible < 1: + break + + def _build_runtime_summary(self, detection_result) -> str: + if not detection_result: + return "运行结果:未获取检测结果" + + summary = getattr(detection_result, 'detection_summary', {}) or {} + threshold_summary = summary.get('threshold_detection') or {} + states = threshold_summary.get('states') or {} + + counts = { + 'on': int(states.get('on', 0)), + 'off': int(states.get('off', 0)), + 'uncertain': int(states.get('uncertain', 0)), + } + + if counts['on'] + counts['off'] + counts['uncertain'] == 0: + stable_states = getattr(detection_result, 'stable_states', {}) or {} + for result in stable_states.values(): + state = getattr(result, 'led_state', None) + if state == LEDState.ON: + counts['on'] += 1 + elif state == LEDState.OFF: + counts['off'] += 1 + else: + counts['uncertain'] += 1 + + total = counts['on'] + counts['off'] + counts['uncertain'] + total_text = f"共{total}盏" if total else "总数未知" + return ( + f"运行结果:亮{counts['on']} 灭{counts['off']} 不确定{counts['uncertain']}({total_text})" + ) + def _cleanup(self) -> None: """ @@ -327,10 +504,50 @@ class YantaiVisionXSystem: if self.display_enabled: cv2.destroyAllWindows() + self._window_created = False self.logger.log_info("系统关闭完成") self.logger.close() + def _ensure_window(self) -> None: + """确保OpenCV窗口存在并设置标题""" + if self._window_created or not self.display_enabled: + return + + cv2.namedWindow(self._window_internal_name, cv2.WINDOW_NORMAL) + cv2.resizeWindow( + self._window_internal_name, + self.tech_ui.window_width, + self.tech_ui.window_height, + ) + cv2.setMouseCallback(self._window_internal_name, self._on_mouse_click) + self._apply_window_title() + self._window_created = True + + def _on_mouse_click(self, event: int, x: int, y: int, flags: int, param) -> None: + """鼠标点击回调函数""" + if event == cv2.EVENT_LBUTTONDOWN: + if self.tech_ui.is_exit_button_clicked(x, y): + self.logger.log_info("用户点击退出按钮") + self.is_running = False + + def _apply_window_title(self) -> None: + """在不同平台上设置窗口标题,优先保证中文显示正常""" + display_title = self._window_display_name + try: + if os.name == 'nt': + import ctypes + + hwnd = ctypes.windll.user32.FindWindowW(None, self._window_internal_name) + if hwnd: + ctypes.windll.user32.SetWindowTextW(hwnd, display_title) + else: + cv2.setWindowTitle(self._window_internal_name, display_title) + else: + cv2.setWindowTitle(self._window_internal_name, display_title) + except Exception: + cv2.setWindowTitle(self._window_internal_name, display_title) + def main(): """ @@ -348,8 +565,16 @@ def main(): help='显示实时检测结果') parser.add_argument('--config', type=str, default='config', help='配置文件目录路径') + parser.add_argument('--ui', action='store_true', + help='启动可视化控制面板') args = parser.parse_args() + + if args.ui: + from src.ui.control_panel import launch_control_panel + + launch_control_panel() + return 0 # 创建系统实例 system = YantaiVisionXSystem(args.config) diff --git a/src/ui/tech_ui.py b/src/ui/tech_ui.py index eb99d7d..d3ee2a9 100644 --- a/src/ui/tech_ui.py +++ b/src/ui/tech_ui.py @@ -18,7 +18,14 @@ from src.roi_detection.threshold_detector import LEDState class TechUI: """高级监控界面""" - def __init__(self, window_width: int = 1440, window_height: int = 900) -> None: + def __init__(self, window_width: int = 1440, window_height: int = 920) -> None: + # 基础画布尺寸(用于绘制后再统一缩放) + self.base_width = 1600 + self.base_height = 1000 + + # 最终输出窗口尺寸(可小于或大于基础尺寸,实现等比例缩放) + self.output_width = window_width + self.output_height = window_height self.window_width = window_width self.window_height = window_height @@ -41,13 +48,14 @@ class TechUI: 'chart_line': (120, 200, 255), } + right_col_x = self.base_width - 470 - 30 self.panels = { - 'header': (0, 0, window_width, 120), - 'video': (24, 130, 860, 520), - 'timeline': (24, 680, 860, 200), - 'status': (910, 130, 520, 230), - 'led_matrix': (910, 380, 520, 250), - 'stats': (910, 660, 520, 240), + 'header': (0, 0, self.base_width, 140), + 'video': (30, 150, 1050, 560), + 'timeline': (30, 720, 1050, 260), + 'status': (right_col_x, 150, 470, 250), + 'led_matrix': (right_col_x, 410, 470, 200), + 'stats': (right_col_x, 620, 470, 360), } self.fonts = { @@ -67,6 +75,9 @@ class TechUI: self.state_history: deque[float] = deque(maxlen=120) self.timeline_events: deque[Dict[str, Any]] = deque(maxlen=12) self.previous_states: Dict[str, LEDState] = {} + + # 退出按钮区域 (将在draw_header中更新) + self.exit_button_rect: Tuple[int, int, int, int] = (0, 0, 0, 0) def _load_chinese_font(self) -> Optional[ImageFont.FreeTypeFont]: font_paths = [ @@ -127,7 +138,7 @@ class TechUI: def create_main_canvas(self) -> np.ndarray: canvas = np.full( - (self.window_height, self.window_width, 3), + (self.base_height, self.base_width, 3), self.colors['bg_dark'], dtype=np.uint8, ) @@ -170,13 +181,22 @@ class TechUI: subtitle = "YantaiVisionX 状态控制台" self._put_chinese_text(canvas, subtitle, (x + 32, y + 68), 18, self.colors['text_secondary']) + # 退出按钮 + btn_w, btn_h = 70, 32 + btn_x = x + w - btn_w - 30 + btn_y = y + 12 + self.exit_button_rect = (btn_x, btn_y, btn_w, btn_h) + cv2.rectangle(canvas, (btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h), self.colors['danger'], -1) + cv2.rectangle(canvas, (btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h), (100, 100, 180), 1) + self._put_chinese_text(canvas, "退出", (btn_x + 16, btn_y + 4), 18, self.colors['text_primary']) + system_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") time_scale = 0.55 time_size = cv2.getTextSize(system_time, self.fonts['mono'], time_scale, 1)[0] cv2.putText( canvas, system_time, - (x + w - time_size[0] - 36, y + 44), + (x + w - time_size[0] - btn_w - 50, y + 44), self.fonts['mono'], time_scale, self.colors['text_secondary'], @@ -249,11 +269,24 @@ class TechUI: mini_size = 18 self._draw_mini_matrix(canvas, x + w - 200, y + h - 120, led_matrix, mini_size) - def draw_status_panel(self, canvas: np.ndarray, detection_result, counts: Dict[str, int]) -> None: + def draw_status_panel( + self, + canvas: np.ndarray, + detection_result, + counts: Dict[str, int], + runtime_message: Optional[str] = None, + ) -> None: x, y, w, h = self.panels['status'] self._draw_glass_panel(canvas, x, y, w, h) self._draw_panel_header(canvas, x, y, "系统状态") + runtime_text = runtime_message or "运行结果:实时检测中" + banner_x = x + 24 + banner_y = y + 58 + banner_w = w - 48 + banner_h = 50 + self._draw_runtime_banner(canvas, banner_x, banner_y, banner_w, banner_h, runtime_text) + fps = self._calc_fps(detection_result) latency = detection_result.processing_time * 1000 if detection_result else 0 stability = ( @@ -261,30 +294,38 @@ class TechUI: if getattr(detection_result, 'detection_summary', None) else 0.0 ) - grid_y = y + 60 - block_w = (w - 80) // 3 + grid_y = banner_y + banner_h + 16 + block_w = (w - 90) // 3 + block_h = 80 metrics = [ ("帧率", f"{fps:.1f}", "FPS", self.colors['accent_cyan']), ("延迟", f"{latency:.1f}", "ms", self.colors['accent_orange']), ("稳定度", f"{stability * 100:.0f}", "%", self.colors['accent_purple']), ] for idx, (label, value, unit, color) in enumerate(metrics): - block_x = x + 30 + idx * (block_w + 10) - self._draw_metric_card(canvas, block_x, grid_y, block_w, 100, label, value, unit, color) + block_x = x + 30 + idx * (block_w + 15) + self._draw_metric_card(canvas, block_x, grid_y, block_w, block_h, label, value, unit, color) mode = self._safe_get_mode(detection_result) - self._put_chinese_text(canvas, f"当前模式: {mode}", (x + 30, y + h - 60), 18, self.colors['text_secondary']) - self._put_chinese_text(canvas, f"帧计数: {getattr(detection_result, 'frame_count', 0)}", (x + 30, y + h - 30), 18, self.colors['text_secondary']) + frame_count = getattr(detection_result, 'frame_count', 0) + + footer_y = grid_y + block_h + 10 + footer_text = f"当前模式: {mode} 帧计数: {frame_count}" + self._put_chinese_text(canvas, footer_text, (x + 30, footer_y), 15, self.colors['text_secondary']) def draw_led_matrix_panel(self, canvas: np.ndarray, led_matrix: List[List[str]], counts: Dict[str, int]) -> None: x, y, w, h = self.panels['led_matrix'] self._draw_glass_panel(canvas, x, y, w, h) - self._draw_panel_header(canvas, x, y, "LED状态矩阵") + + self._put_chinese_text(canvas, "LED状态矩阵", (x + 30, y + 18), 20, self.colors['text_primary']) + summary_text = f"亮 {counts.get('on', 0)} | 灭 {counts.get('off', 0)} | 不确定 {counts.get('uncertain', 0)}" + self._put_chinese_text(canvas, summary_text, (x + 180, y + 20), 14, self.colors['text_secondary']) - matrix_start_x = x + 40 - matrix_start_y = y + 80 - cell = 42 - gap = 14 + cell = 34 + gap = 8 + matrix_width = 6 * cell + 5 * gap + matrix_start_x = x + (w - matrix_width) // 2 + matrix_start_y = y + 55 color_map = { 'on': self.colors['success'], 'off': (80, 90, 120), @@ -301,19 +342,20 @@ class TechUI: cv2.rectangle(canvas, (cell_x, cell_y), (cell_x + cell, cell_y + cell), self.colors['panel_light'], -1) cv2.rectangle(canvas, (cell_x, cell_y), (cell_x + cell, cell_y + cell), color, 2) cv2.circle(canvas, (cell_x + cell // 2, cell_y + cell // 2), cell // 3, color, -1) - cv2.putText( - canvas, - f"R{row + 1}C{col + 1}", - (cell_x - 2, cell_y + cell + 16), - self.fonts['mono'], - 0.3, - self.colors['text_secondary'], - 1, - cv2.LINE_AA, - ) - summary_text = f"亮 {counts.get('on', 0)} | 灭 {counts.get('off', 0)} | 不确定 {counts.get('uncertain', 0)}" - self._put_chinese_text(canvas, summary_text, (x + 30, y + 45), 18, self.colors['text_secondary']) + label_y = matrix_start_y + 3 * (cell + gap) + 12 + for col in range(6): + label_x = matrix_start_x + col * (cell + gap) + cell // 2 - 14 + cv2.putText( + canvas, + f"C{col + 1}", + (label_x, label_y), + self.fonts['mono'], + 0.4, + self.colors['text_secondary'], + 1, + cv2.LINE_AA, + ) def draw_statistics_panel(self, canvas: np.ndarray, detection_result, counts: Dict[str, int]) -> None: x, y, w, h = self.panels['stats'] @@ -324,15 +366,16 @@ class TechUI: on_ratio = counts.get('on', 0) / total off_ratio = counts.get('off', 0) / total - bar_height = 20 - first_bar_y = y + 70 + bar_height = 18 + first_bar_y = y + 80 second_bar_y = first_bar_y + bar_height + 36 self._draw_progress_bar(canvas, x + 30, first_bar_y, w - 60, bar_height, on_ratio, "亮灯占比", self.colors['success']) self._draw_progress_bar(canvas, x + 30, second_bar_y, w - 60, bar_height, off_ratio, "熄灭占比", self.colors['accent_orange']) - chart_top = second_bar_y + bar_height + 34 - chart_height = max(60, y + h - chart_top - 24) - self._draw_trend_chart(canvas, x + 30, chart_top, w - 60, chart_height) + chart_top = second_bar_y + bar_height + 30 + chart_height = y + h - chart_top - 20 + if chart_height > 40: + self._draw_trend_chart(canvas, x + 30, chart_top, w - 60, chart_height) def draw_alarm_timeline(self, canvas: np.ndarray) -> None: x, y, w, h = self.panels['timeline'] @@ -399,6 +442,22 @@ class TechUI: cv2.putText(canvas, value, (x + 10, y + h // 2 + 8), self.fonts['title'], 0.9, self.colors['text_primary'], 2, cv2.LINE_AA) cv2.putText(canvas, unit, (x + 10, y + h - 12), self.fonts['regular'], 0.55, self.colors['text_secondary'], 1, cv2.LINE_AA) + def _draw_runtime_banner( + self, + canvas: np.ndarray, + x: int, + y: int, + w: int, + h: int, + text: str, + ) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['panel_light'], -1, + cv2.LINE_AA) + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['accent_cyan'], 1, + cv2.LINE_AA) + self._put_chinese_text(canvas, text, (x + 14, y + h // 2 - 10), 20, + self.colors['text_primary']) + def _draw_progress_bar( self, canvas: np.ndarray, @@ -569,7 +628,12 @@ class TechUI: mode = mode or getattr(detection_result, 'detection_mode', 'normal') return '雾天' if mode == 'foggy' else '正常' - def create_display_frame(self, video_frame: np.ndarray, detection_result) -> np.ndarray: + def create_display_frame( + self, + video_frame: np.ndarray, + detection_result, + runtime_message: Optional[str] = None, + ) -> np.ndarray: canvas = self.create_main_canvas() counts = self._extract_state_counts(detection_result) self._update_state_history(counts) @@ -579,9 +643,34 @@ class TechUI: self.draw_header(canvas, detection_result, counts) self.draw_video_panel(canvas, video_frame, detection_result, counts, led_matrix) - self.draw_status_panel(canvas, detection_result, counts) + self.draw_status_panel(canvas, detection_result, counts, runtime_message) self.draw_led_matrix_panel(canvas, led_matrix, counts) self.draw_statistics_panel(canvas, detection_result, counts) self.draw_alarm_timeline(canvas) - return canvas \ No newline at end of file + if (self.output_width, self.output_height) != (self.base_width, self.base_height): + interpolation = ( + cv2.INTER_AREA + if self.output_width <= self.base_width and self.output_height <= self.base_height + else cv2.INTER_LINEAR + ) + canvas = cv2.resize(canvas, (self.output_width, self.output_height), interpolation=interpolation) + + return canvas + + def is_exit_button_clicked(self, click_x: int, click_y: int) -> bool: + """检测鼠标点击是否在退出按钮上(考虑缩放)""" + if self.exit_button_rect == (0, 0, 0, 0): + return False + + scale_x = self.output_width / self.base_width + scale_y = self.output_height / self.base_height + + btn_x, btn_y, btn_w, btn_h = self.exit_button_rect + scaled_x = int(btn_x * scale_x) + scaled_y = int(btn_y * scale_y) + scaled_w = int(btn_w * scale_x) + scaled_h = int(btn_h * scale_y) + + return (scaled_x <= click_x <= scaled_x + scaled_w and + scaled_y <= click_y <= scaled_y + scaled_h) \ No newline at end of file