From a8afca716732bb6f7ab1e69a9cc36f7fb93a23e3 Mon Sep 17 00:00:00 2001 From: sladro Date: Mon, 1 Dec 2025 10:43:22 +0800 Subject: [PATCH] refactor: improve TechUI layout spacing Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/ui/tech_ui.py | 816 +++++++++++++++++++++++++++------------------- 1 file changed, 477 insertions(+), 339 deletions(-) diff --git a/src/ui/tech_ui.py b/src/ui/tech_ui.py index 2a7bfbb..eb99d7d 100644 --- a/src/ui/tech_ui.py +++ b/src/ui/tech_ui.py @@ -1,449 +1,587 @@ """ -科技感UI界面模块 -为YantaiVisionX系统提供现代化的用户界面 +SOTA风格的科技感UI界面模块 +为YantaiVisionX提供全屏监控可视化 """ +import os +from collections import deque +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + import cv2 import numpy as np -from datetime import datetime -from typing import Dict, List, Tuple, Optional, Any from PIL import Image, ImageDraw, ImageFont -import os + +from src.roi_detection.threshold_detector import LEDState class TechUI: - """ - 科技感用户界面类 - """ + """高级监控界面""" - def __init__(self, window_width: int = 1280, window_height: int = 720): - """ - 初始化UI界面 - - Args: - window_width: 窗口宽度 - window_height: 窗口高度 - """ + def __init__(self, window_width: int = 1440, window_height: int = 900) -> None: self.window_width = window_width self.window_height = window_height - # 颜色定义 self.colors = { - 'bg_dark': (30, 30, 30), # 深色背景 - 'bg_panel': (45, 45, 45), # 面板背景 - 'accent_blue': (255, 165, 0), # 蓝色强调色 - 'accent_cyan': (255, 255, 0), # 青色强调色 - 'text_primary': (255, 255, 255), # 主要文字 - 'text_secondary': (200, 200, 200), # 次要文字 - 'led_on': (0, 255, 0), # LED开启 - 'led_off': (100, 100, 100), # LED关闭 - 'border': (100, 150, 200), # 边框 - 'warning': (0, 165, 255), # 警告色 - 'success': (0, 255, 0), # 成功色 + 'bg_dark': (18, 20, 38), + 'bg_deep': (26, 32, 55), + 'panel_glass': (40, 52, 78), + 'panel_light': (55, 70, 110), + 'border_soft': (120, 140, 190), + 'accent_cyan': (255, 255, 120), + 'accent_orange': (70, 170, 255), + 'accent_purple': (200, 130, 255), + 'text_primary': (250, 250, 250), + 'text_secondary': (190, 200, 215), + 'success': (0, 215, 140), + 'warning': (0, 215, 255), + 'danger': (70, 70, 255), + 'grid_line': (40, 50, 75), + 'chip_bg': (70, 75, 95), + 'chart_line': (120, 200, 255), } - # 面板区域定义 self.panels = { - 'header': (0, 0, window_width, 80), # 标题区域 - 'video': (20, 100, 800, 480), # 视频显示区域 - 'status': (840, 100, 420, 200), # 状态面板 - 'led_matrix': (840, 320, 420, 200), # LED矩阵显示 - 'stats': (840, 540, 420, 160), # 统计信息 + '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), } - # 字体配置 self.fonts = { 'title': cv2.FONT_HERSHEY_SIMPLEX, - 'subtitle': cv2.FONT_HERSHEY_SIMPLEX, - 'normal': cv2.FONT_HERSHEY_SIMPLEX, - 'small': cv2.FONT_HERSHEY_SIMPLEX, 'mono': cv2.FONT_HERSHEY_DUPLEX, + 'regular': cv2.FONT_HERSHEY_SIMPLEX, } self.font_scales = { - 'title': 1.0, + 'title': 1.05, 'subtitle': 0.7, - 'normal': 0.6, - 'small': 0.5, - 'mono': 0.5, + 'regular': 0.6, + 'small': 0.45, } - # 尝试加载中文字体 self.chinese_font = self._load_chinese_font() + self.state_history: deque[float] = deque(maxlen=120) + self.timeline_events: deque[Dict[str, Any]] = deque(maxlen=12) + self.previous_states: Dict[str, LEDState] = {} - def _load_chinese_font(self): - """加载中文字体""" - # 常见的中文字体路径 + def _load_chinese_font(self) -> Optional[ImageFont.FreeTypeFont]: font_paths = [ - "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 - "C:/Windows/Fonts/simhei.ttf", # 黑体 - "C:/Windows/Fonts/simsun.ttc", # 宋体 - "/System/Library/Fonts/PingFang.ttc", # macOS - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", # Linux + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + "C:/Windows/Fonts/simsun.ttc", + "/System/Library/Fonts/PingFang.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", ] for font_path in font_paths: if os.path.exists(font_path): try: return ImageFont.truetype(font_path, 24) - except: + except Exception: continue - # 如果没有找到字体,使用默认字体 try: return ImageFont.load_default() - except: + except Exception: return None - def _put_chinese_text(self, canvas, text, position, font_size=24, color=(255, 255, 255)): - """在画布上绘制中文文字""" + def _put_chinese_text( + self, + canvas: np.ndarray, + text: str, + position: Tuple[int, int], + font_size: int = 24, + color: Tuple[int, int, int] = (255, 255, 255), + ) -> None: if self.chinese_font is None: - # 如果没有中文字体,使用OpenCV默认字体 - cv2.putText(canvas, text, position, cv2.FONT_HERSHEY_SIMPLEX, - font_size/30, color, 2) + cv2.putText( + canvas, + text, + position, + self.fonts['regular'], + max(font_size / 32, 0.4), + color, + 1, + cv2.LINE_AA, + ) return - # 转换为PIL图像 pil_img = Image.fromarray(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(pil_img) - # 设置字体大小 try: - font = ImageFont.truetype(self.chinese_font.path, font_size) if hasattr(self.chinese_font, 'path') else self.chinese_font - except: + font = ( + ImageFont.truetype(self.chinese_font.path, font_size) + if hasattr(self.chinese_font, 'path') + else self.chinese_font.font_variant(size=font_size) + ) + except Exception: font = self.chinese_font - # 绘制文字 - draw.text(position, text, font=font, fill=color[::-1]) # RGB转BGR - - # 转换回OpenCV格式 - cv2_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) - canvas[:] = cv2_img + draw.text(position, text, font=font, fill=color[::-1]) + canvas[:] = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) def create_main_canvas(self) -> np.ndarray: - """ - 创建主画布 - - Returns: - np.ndarray: 主画布图像 - """ - canvas = np.full((self.window_height, self.window_width, 3), - self.colors['bg_dark'], dtype=np.uint8) - - # 绘制渐变背景效果 + canvas = np.full( + (self.window_height, self.window_width, 3), + self.colors['bg_dark'], + dtype=np.uint8, + ) self._draw_gradient_background(canvas) - return canvas - def _draw_gradient_background(self, canvas: np.ndarray): - """绘制渐变背景""" + def _draw_gradient_background(self, canvas: np.ndarray) -> None: h, w = canvas.shape[:2] - # 创建微妙的渐变效果 for i in range(h): - intensity = int(30 + (i / h) * 10) # 从30到40的渐变 - canvas[i:i+1, :] = (intensity, intensity, intensity) + ratio = i / max(h - 1, 1) + base = int(20 + ratio * 25) + canvas[i, :, :] = (base, base + 2, base + 8) - # 添加网格线效果 - for i in range(0, w, 40): - cv2.line(canvas, (i, 0), (i, h), (40, 40, 40), 1) - for i in range(0, h, 40): - cv2.line(canvas, (0, i), (w, i), (40, 40, 40), 1) + for x in range(0, w, 48): + cv2.line(canvas, (x, 0), (x, h), self.colors['grid_line'], 1) + for y in range(0, h, 48): + cv2.line(canvas, (0, y), (w, y), self.colors['grid_line'], 1) - def draw_header(self, canvas: np.ndarray, system_time: str = None): - """ - 绘制标题头部 + overlay = canvas.copy() + cv2.circle( + overlay, + (int(w * 0.35), int(h * 0.25)), + int(min(w, h) * 0.5), + (35, 45, 80), + -1, + ) + cv2.addWeighted(overlay, 0.25, canvas, 0.75, 0, canvas) - Args: - canvas: 画布 - system_time: 系统时间 - """ + def draw_header(self, canvas: np.ndarray, detection_result, counts: Dict[str, int]) -> None: x, y, w, h = self.panels['header'] + header = canvas[y:y + h, x:x + w] + overlay = header.copy() + cv2.rectangle(overlay, (0, 0), (w - 1, h - 1), self.colors['bg_deep'], -1) + cv2.addWeighted(overlay, 0.85, header, 0.15, 0, header) + cv2.line(header, (20, h - 8), (w - 20, h - 8), self.colors['accent_cyan'], 2) - # 绘制标题背景 - cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['bg_panel'], -1) - cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['border'], 2) + title = "烟台蓬莱国际机场 · 低能见度灯阵监控" + self._put_chinese_text(canvas, title, (x + 30, y + 26), 28, self.colors['text_primary']) + subtitle = "YantaiVisionX 状态控制台" + self._put_chinese_text(canvas, subtitle, (x + 32, y + 68), 18, self.colors['text_secondary']) - # 主标题 - title = "烟台蓬莱国际机场低能见度识别软件" + 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), + self.fonts['mono'], + time_scale, + self.colors['text_secondary'], + 1, + cv2.LINE_AA, + ) - # 使用中文字体绘制标题 - title_x = x + 50 # 简化定位 - title_y = y + 20 + fps = self._calc_fps(detection_result) + chips = [ + ("亮灯", f"{counts.get('on', 0)}", self.colors['success']), + ("灭灯", f"{counts.get('off', 0)}", self.colors['accent_orange']), + ("FPS", f"{fps:.1f}", self.colors['accent_cyan']), + ] - # 标题阴影效果 - self._put_chinese_text(canvas, title, (title_x + 2, title_y + 2), 28, (0, 0, 0)) - self._put_chinese_text(canvas, title, (title_x, title_y), 28, self.colors['accent_cyan']) + chip_spacing = 20 + dynamic_width = (w - 420 - chip_spacing * (len(chips) - 1)) // max(len(chips), 1) + chip_width = max(120, min(170, dynamic_width)) + chip_height = 46 + start_x = x + w - (chip_width * len(chips) + chip_spacing * (len(chips) - 1)) - 34 + for idx, (label, value, color) in enumerate(chips): + chip_x = start_x + idx * (chip_width + chip_spacing) + chip_y = y + h - chip_height - 24 + self._draw_chip(canvas, chip_x, chip_y, chip_width, chip_height, label, value, color) - # 副标题 - subtitle = "灯阵监控系统" - self._put_chinese_text(canvas, subtitle, (title_x + 150, title_y + 35), 18, self.colors['text_secondary']) - - # 时间显示 - if system_time is None: - system_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - time_x = x + w - 200 - time_y = y + 25 - cv2.putText(canvas, system_time, (time_x, time_y), - self.fonts['small'], self.font_scales['small'], - self.colors['text_secondary'], 1) - - # 绘制装饰线 - cv2.line(canvas, (x + 20, y + h - 5), (x + w - 20, y + h - 5), - self.colors['accent_blue'], 2) - - def draw_video_panel(self, canvas: np.ndarray, video_frame: np.ndarray): - """ - 绘制视频显示面板 - - Args: - canvas: 主画布 - video_frame: 视频帧 - """ + def draw_video_panel( + self, + canvas: np.ndarray, + video_frame: Optional[np.ndarray], + detection_result, + counts: Dict[str, int], + led_matrix: List[List[str]], + ) -> None: x, y, w, h = self.panels['video'] + roi = canvas[y:y + h, x:x + w] + overlay = roi.copy() + cv2.rectangle(overlay, (0, 0), (w - 1, h - 1), self.colors['panel_glass'], -1) + cv2.addWeighted(overlay, 0.35, roi, 0.65, 0, roi) - # 绘制面板背景和边框 - cv2.rectangle(canvas, (x - 2, y - 2), (x + w + 2, y + h + 2), - self.colors['border'], 2) - - # 调整视频帧大小并显示 if video_frame is not None: resized_frame = cv2.resize(video_frame, (w, h)) - canvas[y:y+h, x:x+w] = resized_frame + canvas[y:y + h, x:x + w] = resized_frame else: - # 无视频时显示占位符 - cv2.rectangle(canvas, (x, y), (x + w, y + h), - self.colors['bg_panel'], -1) - placeholder_text = "无视频信号" - text_x = x + w // 2 - 40 - text_y = y + h // 2 - self._put_chinese_text(canvas, placeholder_text, (text_x, text_y), 18, self.colors['text_secondary']) + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['panel_light'], -1) + self._put_chinese_text(canvas, "无视频信号", (x + w // 2 - 80, y + h // 2 - 10), 24, self.colors['text_secondary']) - # 绘制视频标签 - cv2.rectangle(canvas, (x, y - 25), (x + 100, y - 2), - self.colors['bg_panel'], -1) - self._put_chinese_text(canvas, "实时视频", (x + 10, y - 22), 14, self.colors['accent_cyan']) + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['border_soft'], 2) + self._draw_video_hud(canvas, x, y, w, h, detection_result, counts, led_matrix) - def draw_status_panel(self, canvas: np.ndarray, detection_result): - """ - 绘制系统状态面板 + def _draw_video_hud( + self, + canvas: np.ndarray, + x: int, + y: int, + w: int, + h: int, + detection_result, + counts: Dict[str, int], + led_matrix: List[List[str]], + ) -> None: + fps = self._calc_fps(detection_result) + mode = self._safe_get_mode(detection_result) + latency = detection_result.processing_time * 1000 if detection_result else 0 - Args: - canvas: 画布 - detection_result: 检测结果 - """ + hud_x = x + 30 + hud_y = y + 40 + self._put_chinese_text(canvas, "航站区 · 三排灯阵", (hud_x, hud_y), 20, self.colors['text_primary']) + info = f"模式 {mode} | 帧率 {fps:.1f} FPS | 延迟 {latency:.1f} ms" + self._put_chinese_text(canvas, info, (hud_x, hud_y + 32), 18, self.colors['text_secondary']) + + 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: x, y, w, h = self.panels['status'] + self._draw_glass_panel(canvas, x, y, w, h) + self._draw_panel_header(canvas, x, y, "系统状态") - # 绘制面板 - self._draw_panel(canvas, "系统状态", x, y, w, h) + fps = self._calc_fps(detection_result) + latency = detection_result.processing_time * 1000 if detection_result else 0 + stability = ( + detection_result.detection_summary.get('stability_info', {}).get('avg_stability', 0.0) + if getattr(detection_result, 'detection_summary', None) + else 0.0 + ) + grid_y = y + 60 + block_w = (w - 80) // 3 + 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) - # 状态信息 - status_y = y + 40 - line_height = 25 + 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_text = f"帧数: {detection_result.frame_count}" - self._put_chinese_text(canvas, frame_text, (x + 15, status_y), 14, self.colors['text_primary']) - - # 处理时间 - status_y += line_height - processing_time = detection_result.processing_time * 1000 - time_text = f"处理时间: {processing_time:.1f}毫秒" - self._put_chinese_text(canvas, time_text, (x + 15, status_y), 14, self.colors['text_primary']) - - # FPS计算 - status_y += line_height - fps = 1.0 / detection_result.processing_time if detection_result.processing_time > 0 else 0 - fps_text = f"帧率: {fps:.1f}FPS" - self._put_chinese_text(canvas, fps_text, (x + 15, status_y), 14, self.colors['text_primary']) - - # 检测模式 - status_y += line_height - detection_mode = getattr(detection_result, 'detection_mode', 'normal') - mode_name = "正常" if detection_mode == "normal" else "雾天" - mode_text = f"模式: {mode_name}" - self._put_chinese_text(canvas, mode_text, (x + 15, status_y), 14, self.colors['accent_cyan']) - - # 系统状态指示灯 - self._draw_status_indicator(canvas, x + w - 80, y + 30, "在线", True) - - def draw_led_matrix_panel(self, canvas: np.ndarray, led_states: List[List[bool]]): - """ - 绘制LED状态矩阵面板 - - Args: - canvas: 画布 - led_states: LED状态矩阵 (3x6) - """ + 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._draw_panel(canvas, "LED状态矩阵", x, y, w, h) - - # LED矩阵绘制 - matrix_start_x = x + 20 - matrix_start_y = y + 50 - led_size = 25 - led_spacing = 35 + matrix_start_x = x + 40 + matrix_start_y = y + 80 + cell = 42 + gap = 14 + color_map = { + 'on': self.colors['success'], + 'off': (80, 90, 120), + 'uncertain': self.colors['warning'], + } for row in range(3): for col in range(6): - led_x = matrix_start_x + col * (led_size + led_spacing) - led_y = matrix_start_y + row * (led_size + led_spacing) + cell_x = matrix_start_x + col * (cell + gap) + cell_y = matrix_start_y + row * (cell + gap) + state = led_matrix[row][col] if row < len(led_matrix) else 'uncertain' + color = color_map.get(state, self.colors['warning']) - # 确定LED状态 - is_on = False - if row < len(led_states) and col < len(led_states[row]): - is_on = led_states[row][col] + 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, + ) - # 绘制LED - led_color = self.colors['led_on'] if is_on else self.colors['led_off'] - cv2.circle(canvas, (led_x + led_size // 2, led_y + led_size // 2), - led_size // 2, led_color, -1) + 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']) - # LED边框 - border_color = self.colors['success'] if is_on else self.colors['border'] - cv2.circle(canvas, (led_x + led_size // 2, led_y + led_size // 2), - led_size // 2, border_color, 2) - - # LED标签 - label = f"R{row+1}C{col+1}" - cv2.putText(canvas, label, (led_x - 5, led_y + led_size + 15), - self.fonts['small'], 0.3, - self.colors['text_secondary'], 1) - - def draw_statistics_panel(self, canvas: np.ndarray, detection_result): - """ - 绘制统计信息面板 - - Args: - canvas: 画布 - detection_result: 检测结果 - """ + def draw_statistics_panel(self, canvas: np.ndarray, detection_result, counts: Dict[str, int]) -> None: x, y, w, h = self.panels['stats'] + self._draw_glass_panel(canvas, x, y, w, h) + self._draw_panel_header(canvas, x, y, "运行指标") - # 绘制面板 - self._draw_panel(canvas, "统计信息", x, y, w, h) + total = max(counts.get('total', 0), 1) + on_ratio = counts.get('on', 0) / total + off_ratio = counts.get('off', 0) / total - # 统计信息 - stats_y = y + 40 - line_height = 22 + bar_height = 20 + first_bar_y = y + 70 + 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']) - # 获取统计数据 - summary = detection_result.detection_summary - if 'threshold_detection' in summary: - states = summary['threshold_detection']['states'] - total_on = states.get('on', 0) - total_off = states.get('off', 0) - total_leds = total_on + total_off - else: - total_on = total_off = total_leds = 0 + 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) - # LED统计 - self._put_chinese_text(canvas, f"总LED数: {total_leds}", (x + 15, stats_y), 14, self.colors['text_primary']) + def draw_alarm_timeline(self, canvas: np.ndarray) -> None: + x, y, w, h = self.panels['timeline'] + self._draw_glass_panel(canvas, x, y, w, h) + self._draw_panel_header(canvas, x, y, "状态变更时间线") - stats_y += line_height - self._put_chinese_text(canvas, f"开启: {total_on}盏", (x + 15, stats_y), 14, self.colors['success']) + start_y = y + 70 + entry_height = 42 - stats_y += line_height - self._put_chinese_text(canvas, f"关闭: {total_off}盏", (x + 15, stats_y), 14, self.colors['led_off']) + if not self.timeline_events: + self._put_chinese_text(canvas, "暂无状态变化,系统稳定运行", (x + 30, start_y), 20, self.colors['text_secondary']) + return - # 运行时间 - stats_y += line_height + 5 - runtime_text = f"运行时间: {datetime.now().strftime('%H:%M:%S')}" - self._put_chinese_text(canvas, runtime_text, (x + 15, stats_y), 14, self.colors['text_secondary']) + for idx, event in enumerate(list(self.timeline_events)): + entry_y = start_y + idx * entry_height + if entry_y + entry_height > y + h - 10: + break + self._draw_timeline_entry(canvas, x + 30, entry_y, w - 60, entry_height - 6, event) - def _draw_panel(self, canvas: np.ndarray, title: str, x: int, y: int, w: int, h: int): - """ - 绘制通用面板 + def _draw_glass_panel(self, canvas: np.ndarray, x: int, y: int, w: int, h: int) -> None: + roi = canvas[y:y + h, x:x + w] + overlay = roi.copy() + cv2.rectangle(overlay, (0, 0), (w - 1, h - 1), self.colors['panel_glass'], -1) + cv2.addWeighted(overlay, 0.78, roi, 0.22, 0, roi) + cv2.rectangle(roi, (0, 0), (w - 1, h - 1), self.colors['border_soft'], 1) - Args: - canvas: 画布 - title: 面板标题 - x, y, w, h: 面板位置和大小 - """ - # 面板背景 - cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['bg_panel'], -1) - cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['border'], 2) + def _draw_panel_header(self, canvas: np.ndarray, x: int, y: int, title: str) -> None: + text_top = y + 22 + self._put_chinese_text(canvas, title, (x + 30, text_top), 22, self.colors['text_primary']) + underline_y = text_top + 26 + cv2.line(canvas, (x + 30, underline_y), (x + 150, underline_y), self.colors['accent_cyan'], 2) - # 标题栏 - cv2.rectangle(canvas, (x, y), (x + w, y + 30), self.colors['accent_blue'], -1) - self._put_chinese_text(canvas, title, (x + 10, y + 5), 16, self.colors['text_primary']) + def _draw_chip( + self, + canvas: np.ndarray, + x: int, + y: int, + w: int, + h: int, + label: str, + value: str, + color: Tuple[int, int, int], + ) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['chip_bg'], -1, cv2.LINE_AA) + cv2.rectangle(canvas, (x, y), (x + w, y + h), color, 1, cv2.LINE_AA) + self._put_chinese_text(canvas, label, (x + 12, y + 6), 16, color) + cv2.putText(canvas, value, (x + 12, y + h - 8), self.fonts['mono'], 0.6, self.colors['text_primary'], 1, cv2.LINE_AA) - # 装饰线 - cv2.line(canvas, (x + 5, y + 32), (x + w - 5, y + 32), - self.colors['accent_cyan'], 1) + def _draw_metric_card( + self, + canvas: np.ndarray, + x: int, + y: int, + w: int, + h: int, + label: str, + value: str, + unit: str, + color: Tuple[int, int, int], + ) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['panel_light'], -1) + cv2.rectangle(canvas, (x, y), (x + w, y + h), color, 1) + self._put_chinese_text(canvas, label, (x + 10, y + 6), 18, color) + 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_status_indicator(self, canvas: np.ndarray, x: int, y: int, - text: str, is_active: bool): - """ - 绘制状态指示器 + def _draw_progress_bar( + self, + canvas: np.ndarray, + x: int, + y: int, + w: int, + h: int, + ratio: float, + label: str, + color: Tuple[int, int, int], + ) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['chip_bg'], -1) + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['border_soft'], 1) + filled = int(w * max(0.0, min(1.0, ratio))) + cv2.rectangle(canvas, (x, y), (x + filled, y + h), color, -1) + self._put_chinese_text(canvas, f"{label} {ratio * 100:.0f}%", (x, y - 24), 18, self.colors['text_secondary']) - Args: - canvas: 画布 - x, y: 位置 - text: 状态文本 - is_active: 是否激活状态 - """ - # 指示灯 - color = self.colors['success'] if is_active else self.colors['warning'] - cv2.circle(canvas, (x, y), 6, color, -1) - cv2.circle(canvas, (x, y), 6, self.colors['border'], 1) + def _draw_trend_chart(self, canvas: np.ndarray, x: int, y: int, w: int, h: int) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['chip_bg'], -1) + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['border_soft'], 1) + if len(self.state_history) < 2: + self._put_chinese_text(canvas, "趋势数据不足", (x + 10, y + h // 2 - 10), 18, self.colors['text_secondary']) + return - # 状态文本 - self._put_chinese_text(canvas, text, (x + 15, y - 5), 12, color) + points = list(self.state_history) + max_idx = len(points) - 1 + for idx in range(max_idx): + start_ratio = points[idx] + end_ratio = points[idx + 1] + start_x = x + int((idx / max_idx) * w) + end_x = x + int(((idx + 1) / max_idx) * w) + start_y = y + h - int(start_ratio * (h - 10)) - 5 + end_y = y + h - int(end_ratio * (h - 10)) - 5 + cv2.line(canvas, (start_x, start_y), (end_x, end_y), self.colors['chart_line'], 2) + + def _draw_mini_matrix(self, canvas: np.ndarray, x: int, y: int, matrix: List[List[str]], cell: int) -> None: + color_map = { + 'on': self.colors['success'], + 'off': (90, 90, 120), + 'uncertain': self.colors['warning'], + } + for row in range(3): + for col in range(6): + cell_x = x + col * (cell + 6) + cell_y = y + row * (cell + 6) + state = matrix[row][col] if row < len(matrix) else 'uncertain' + color = color_map.get(state, self.colors['warning']) + cv2.rectangle(canvas, (cell_x, cell_y), (cell_x + cell, cell_y + cell), color, -1) + + def _draw_timeline_entry(self, canvas: np.ndarray, x: int, y: int, w: int, h: int, event: Dict[str, Any]) -> None: + cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['panel_light'], -1) + state_color, state_label = self._state_to_color_and_label(event.get('state')) + cv2.rectangle(canvas, (x, y), (x + 6, y + h), state_color, -1) + self._put_chinese_text(canvas, f"{event.get('roi', '')}", (x + 16, y + 6), 18, self.colors['text_primary']) + self._put_chinese_text(canvas, state_label, (x + 120, y + 6), 18, state_color) + time_text = self._format_event_time(event.get('timestamp'), event.get('frame')) + cv2.putText(canvas, time_text, (x + w - 190, y + h - 10), self.fonts['mono'], 0.45, self.colors['text_secondary'], 1, cv2.LINE_AA) + + def _state_to_color_and_label(self, state: Optional[LEDState]) -> Tuple[Tuple[int, int, int], str]: + if state == LEDState.ON: + return self.colors['success'], "亮" + if state == LEDState.OFF: + return self.colors['accent_orange'], "灭" + return self.colors['warning'], "不确定" + + def _format_event_time(self, timestamp: Optional[float], frame: Optional[int]) -> str: + if timestamp is None: + return f"Frame {frame or 0}" + ts = datetime.fromtimestamp(timestamp).strftime("%H:%M:%S") + frame_part = f"#{frame}" if frame is not None else "" + return f"{ts} {frame_part}" + + def _extract_state_counts(self, detection_result) -> Dict[str, Any]: + summary = getattr(detection_result, 'detection_summary', {}) or {} + threshold_summary = summary.get('threshold_detection') or {} + states = threshold_summary.get('states') or {} + + on_count = int(states.get('on', 0)) + off_count = int(states.get('off', 0)) + uncertain_count = int(states.get('uncertain', 0)) + + if not states: + stable_states = getattr(detection_result, 'stable_states', {}) or {} + on_count = off_count = uncertain_count = 0 + for result in stable_states.values(): + if result.led_state == LEDState.ON: + on_count += 1 + elif result.led_state == LEDState.OFF: + off_count += 1 + else: + uncertain_count += 1 + + total = summary.get('frame_info', {}).get('total_rois') or threshold_summary.get('total_leds') + if total is None: + total = on_count + off_count + uncertain_count + + avg_confidence = 0.0 + stable_states = getattr(detection_result, 'stable_states', {}) or {} + if stable_states: + avg_confidence = sum(getattr(s, 'confidence', 0.0) for s in stable_states.values()) / len(stable_states) + + return { + 'total': int(total or 0), + 'on': on_count, + 'off': off_count, + 'uncertain': uncertain_count, + 'avg_confidence': avg_confidence, + } + + def _convert_to_matrix(self, detection_result) -> List[List[str]]: + matrix = [[ 'uncertain' for _ in range(6)] for _ in range(3)] + stable_states = getattr(detection_result, 'stable_states', {}) or {} + + for roi_name, stable in stable_states.items(): + if not roi_name.startswith('R') or 'C' not in roi_name: + continue + try: + row = int(roi_name[1]) - 1 + col = int(roi_name[3]) - 1 + except (ValueError, IndexError): + continue + if 0 <= row < 3 and 0 <= col < 6: + if stable.led_state == LEDState.ON: + matrix[row][col] = 'on' + elif stable.led_state == LEDState.OFF: + matrix[row][col] = 'off' + else: + matrix[row][col] = 'uncertain' + + return matrix + + def _update_state_history(self, counts: Dict[str, Any]) -> None: + total = counts.get('total', 0) + if total: + ratio = counts.get('on', 0) / total + self.state_history.append(ratio) + + def _update_timeline(self, detection_result) -> None: + stable_states = getattr(detection_result, 'stable_states', {}) or {} + current = {roi: result.led_state for roi, result in stable_states.items()} + + if not self.previous_states: + self.previous_states = current + return + + for roi_name, state in current.items(): + prev_state = self.previous_states.get(roi_name) + if prev_state and prev_state != state: + self.timeline_events.appendleft( + { + 'roi': roi_name, + 'state': state, + 'timestamp': getattr(detection_result, 'timestamp', None), + 'frame': getattr(detection_result, 'frame_count', None), + } + ) + + self.previous_states = current + + def _calc_fps(self, detection_result) -> float: + if not detection_result or detection_result.processing_time <= 0: + return 0.0 + return 1.0 / detection_result.processing_time + + def _safe_get_mode(self, detection_result) -> str: + summary = getattr(detection_result, 'detection_summary', {}) or {} + mode = summary.get('frame_info', {}).get('detection_mode') + 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: - """ - 创建完整的显示帧 - - Args: - video_frame: 原始视频帧 - detection_result: 检测结果 - - Returns: - np.ndarray: 完整的显示帧 - """ - # 创建主画布 canvas = self.create_main_canvas() + counts = self._extract_state_counts(detection_result) + self._update_state_history(counts) + self._update_timeline(detection_result) - # 绘制各个组件 - self.draw_header(canvas) - self.draw_video_panel(canvas, video_frame) - self.draw_status_panel(canvas, detection_result) + led_matrix = self._convert_to_matrix(detection_result) - # 转换LED状态为矩阵格式 - led_states = self._convert_to_matrix(detection_result) - self.draw_led_matrix_panel(canvas, led_states) + 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_led_matrix_panel(canvas, led_matrix, counts) + self.draw_statistics_panel(canvas, detection_result, counts) + self.draw_alarm_timeline(canvas) - self.draw_statistics_panel(canvas, detection_result) - - return canvas - - def _convert_to_matrix(self, detection_result) -> List[List[bool]]: - """ - 将检测结果转换为3x6矩阵格式 - - Args: - detection_result: 检测结果 - - Returns: - List[List[bool]]: LED状态矩阵 - """ - matrix = [[False] * 6 for _ in range(3)] - - # 从检测结果中提取LED状态 - if hasattr(detection_result, 'roi_detections'): - for roi_name, detection in detection_result.roi_detections.items(): - if roi_name.startswith('R') and 'C' in roi_name: - try: - row = int(roi_name[1]) - 1 - col = int(roi_name[3]) - 1 - if 0 <= row < 3 and 0 <= col < 6: - is_on = detection.get('threshold_detection', {}).get('is_on', False) - matrix[row][col] = is_on - except (ValueError, IndexError): - continue - - return matrix \ No newline at end of file + return canvas \ No newline at end of file