diff --git a/main.py b/main.py index 5c8a80d..b92d914 100644 --- a/main.py +++ b/main.py @@ -23,6 +23,7 @@ from src.preprocessing.image_enhancer import ImageEnhancer from src.roi_detection.led_detector import LEDDetector from src.output.result_formatter import ResultFormatter from src.output.logger import LEDLogger +from src.ui.tech_ui import TechUI class YantaiVisionXSystem: @@ -46,7 +47,8 @@ class YantaiVisionXSystem: self.led_detector = None self.formatter = ResultFormatter() self.logger = LEDLogger() - + self.tech_ui = TechUI() + # 状态变量 self.is_running = False self.display_enabled = False @@ -255,38 +257,27 @@ class YantaiVisionXSystem: def _display_results(self, original_frame, detection_result) -> None: """ 显示检测结果 - + Args: original_frame: 原始帧 detection_result: 检测结果 """ - # 可视化检测结果 + # 可视化检测结果(在原始帧上绘制ROI等信息) vis_frame = self.led_detector.visualize_detection_result( original_frame, detection_result ) - - # 添加状态信息 - info_text = f"Frame: {detection_result.frame_count} | " - info_text += f"Time: {detection_result.processing_time*1000:.1f}ms | " - - summary = detection_result.detection_summary - if 'threshold_detection' in summary: - states = summary['threshold_detection']['states'] - info_text += f"ON: {states.get('on', 0)} | OFF: {states.get('off', 0)}" - - cv2.putText(vis_frame, info_text, (10, 30), - cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - - # 显示状态矩阵 - matrix_text = self.formatter.format_matrix_visual( - self.formatter.format_to_matrix(detection_result) - ) - - # 显示图像和状态 - cv2.imshow('YantaiVisionX - LED Detection', vis_frame) - + + # 使用科技感UI创建完整显示帧 + display_frame = self.tech_ui.create_display_frame(vis_frame, detection_result) + + # 显示图像 + cv2.imshow('YantaiVisionX LED Detection System', display_frame) + # 在控制台显示矩阵状态(每10帧显示一次) if detection_result.frame_count % 10 == 0: + matrix_text = self.formatter.format_matrix_visual( + self.formatter.format_to_matrix(detection_result) + ) print(f"\n{matrix_text}") def _cleanup(self) -> None: diff --git a/requirements.txt b/requirements.txt index f8f08b5..1a2b912 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ opencv-python>=4.8.0 numpy>=1.24.0 scikit-image>=0.20.0 -matplotlib>=3.7.0 \ No newline at end of file +matplotlib>=3.7.0 +Pillow>=9.5.0 +PyYAML>=6.0 \ No newline at end of file diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e08e8fd --- /dev/null +++ b/src/ui/__init__.py @@ -0,0 +1,8 @@ +""" +UI模块 +为YantaiVisionX系统提供用户界面组件 +""" + +from .tech_ui import TechUI + +__all__ = ['TechUI'] \ No newline at end of file diff --git a/src/ui/tech_ui.py b/src/ui/tech_ui.py new file mode 100644 index 0000000..ec29930 --- /dev/null +++ b/src/ui/tech_ui.py @@ -0,0 +1,480 @@ +""" +科技感UI界面模块 +为YantaiVisionX系统提供现代化的用户界面 +""" + +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 + + +class TechUI: + """ + 科技感用户界面类 + """ + + def __init__(self, window_width: int = 1280, window_height: int = 720): + """ + 初始化UI界面 + + Args: + window_width: 窗口宽度 + window_height: 窗口高度 + """ + 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), # 成功色 + } + + # 面板区域定义 + 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), # 统计信息 + } + + # 字体配置 + 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, + } + + self.font_scales = { + 'title': 1.0, + 'subtitle': 0.7, + 'normal': 0.6, + 'small': 0.5, + 'mono': 0.5, + } + + # 尝试加载中文字体 + self.chinese_font = self._load_chinese_font() + + def _load_chinese_font(self): + """加载中文字体""" + # 常见的中文字体路径 + 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 + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + return ImageFont.truetype(font_path, 24) + except: + continue + + # 如果没有找到字体,使用默认字体 + try: + return ImageFont.load_default() + except: + return None + + def _put_chinese_text(self, canvas, text, position, font_size=24, color=(255, 255, 255)): + """在画布上绘制中文文字""" + if self.chinese_font is None: + # 如果没有中文字体,使用OpenCV默认字体 + cv2.putText(canvas, text, position, cv2.FONT_HERSHEY_SIMPLEX, + font_size/30, color, 2) + 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 = 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 + + 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) + + # 绘制渐变背景效果 + self._draw_gradient_background(canvas) + + return canvas + + def _draw_gradient_background(self, canvas: np.ndarray): + """绘制渐变背景""" + 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) + + # 添加网格线效果 + 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) + + def draw_header(self, canvas: np.ndarray, system_time: str = None): + """ + 绘制标题头部 + + Args: + canvas: 画布 + system_time: 系统时间 + """ + x, y, w, h = self.panels['header'] + + # 绘制标题背景 + 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 = "烟台蓬莱国际机场低能见度识别软件" + + # 使用中文字体绘制标题 + title_x = x + 50 # 简化定位 + title_y = y + 20 + + # 标题阴影效果 + 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']) + + # 副标题 + subtitle = "YantaiVisionX LED Array Monitoring System" + subtitle_size = cv2.getTextSize(subtitle, self.fonts['subtitle'], + self.font_scales['subtitle'], 1)[0] + subtitle_x = x + (w - subtitle_size[0]) // 2 + subtitle_y = y + 60 + cv2.putText(canvas, subtitle, (subtitle_x, subtitle_y), + self.fonts['subtitle'], self.font_scales['subtitle'], + self.colors['text_secondary'], 1) + + # 时间显示 + 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: 视频帧 + """ + x, y, w, h = self.panels['video'] + + # 绘制面板背景和边框 + 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 + else: + # 无视频时显示占位符 + cv2.rectangle(canvas, (x, y), (x + w, y + h), + self.colors['bg_panel'], -1) + placeholder_text = "NO VIDEO SIGNAL" + text_size = cv2.getTextSize(placeholder_text, self.fonts['normal'], + self.font_scales['normal'], 2)[0] + text_x = x + (w - text_size[0]) // 2 + text_y = y + (h + text_size[1]) // 2 + cv2.putText(canvas, placeholder_text, (text_x, text_y), + self.fonts['normal'], self.font_scales['normal'], + self.colors['text_secondary'], 2) + + # 绘制视频标签 + cv2.rectangle(canvas, (x, y - 25), (x + 120, y - 2), + self.colors['bg_panel'], -1) + cv2.putText(canvas, "LIVE VIDEO", (x + 10, y - 8), + self.fonts['small'], self.font_scales['small'], + self.colors['accent_cyan'], 1) + + def draw_status_panel(self, canvas: np.ndarray, detection_result): + """ + 绘制系统状态面板 + + Args: + canvas: 画布 + detection_result: 检测结果 + """ + x, y, w, h = self.panels['status'] + + # 绘制面板 + self._draw_panel(canvas, "SYSTEM STATUS", x, y, w, h) + + # 状态信息 + status_y = y + 40 + line_height = 25 + + # 帧计数 + frame_text = f"Frame: {detection_result.frame_count}" + cv2.putText(canvas, frame_text, (x + 15, status_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['text_primary'], 1) + + # 处理时间 + status_y += line_height + processing_time = detection_result.processing_time * 1000 + time_text = f"Processing: {processing_time:.1f}ms" + cv2.putText(canvas, time_text, (x + 15, status_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['text_primary'], 1) + + # FPS计算 + status_y += line_height + fps = 1.0 / detection_result.processing_time if detection_result.processing_time > 0 else 0 + fps_text = f"FPS: {fps:.1f}" + cv2.putText(canvas, fps_text, (x + 15, status_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['text_primary'], 1) + + # 检测模式 + status_y += line_height + detection_mode = getattr(detection_result, 'detection_mode', 'normal') + mode_text = f"Mode: {detection_mode.upper()}" + cv2.putText(canvas, mode_text, (x + 15, status_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['accent_cyan'], 1) + + # 系统状态指示灯 + self._draw_status_indicator(canvas, x + w - 60, y + 30, "ONLINE", True) + + def draw_led_matrix_panel(self, canvas: np.ndarray, led_states: List[List[bool]]): + """ + 绘制LED状态矩阵面板 + + Args: + canvas: 画布 + led_states: LED状态矩阵 (3x6) + """ + x, y, w, h = self.panels['led_matrix'] + + # 绘制面板 + self._draw_panel(canvas, "LED STATUS MATRIX", x, y, w, h) + + # LED矩阵绘制 + matrix_start_x = x + 20 + matrix_start_y = y + 50 + led_size = 25 + led_spacing = 35 + + 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) + + # 确定LED状态 + is_on = False + if row < len(led_states) and col < len(led_states[row]): + is_on = led_states[row][col] + + # 绘制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) + + # 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: 检测结果 + """ + x, y, w, h = self.panels['stats'] + + # 绘制面板 + self._draw_panel(canvas, "STATISTICS", x, y, w, h) + + # 统计信息 + stats_y = y + 40 + line_height = 22 + + # 获取统计数据 + 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 + + # LED统计 + cv2.putText(canvas, f"Total LEDs: {total_leds}", (x + 15, stats_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['text_primary'], 1) + + stats_y += line_height + cv2.putText(canvas, f"LEDs ON: {total_on}", (x + 15, stats_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['success'], 1) + + stats_y += line_height + cv2.putText(canvas, f"LEDs OFF: {total_off}", (x + 15, stats_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['led_off'], 1) + + # 运行时间 + stats_y += line_height + 5 + runtime_text = f"Runtime: {datetime.now().strftime('%H:%M:%S')}" + cv2.putText(canvas, runtime_text, (x + 15, stats_y), + self.fonts['mono'], self.font_scales['mono'], + self.colors['text_secondary'], 1) + + def _draw_panel(self, canvas: np.ndarray, title: str, x: int, y: int, w: int, h: int): + """ + 绘制通用面板 + + 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) + + # 标题栏 + cv2.rectangle(canvas, (x, y), (x + w, y + 30), self.colors['accent_blue'], -1) + cv2.putText(canvas, title, (x + 10, y + 20), + self.fonts['subtitle'], self.font_scales['small'], + self.colors['text_primary'], 1) + + # 装饰线 + cv2.line(canvas, (x + 5, y + 32), (x + w - 5, y + 32), + self.colors['accent_cyan'], 1) + + def _draw_status_indicator(self, canvas: np.ndarray, x: int, y: int, + text: str, is_active: bool): + """ + 绘制状态指示器 + + 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) + + # 状态文本 + cv2.putText(canvas, text, (x + 15, y + 5), + self.fonts['small'], self.font_scales['small'], + color, 1) + + 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() + + # 绘制各个组件 + self.draw_header(canvas) + self.draw_video_panel(canvas, video_frame) + self.draw_status_panel(canvas, detection_result) + + # 转换LED状态为矩阵格式 + led_states = self._convert_to_matrix(detection_result) + self.draw_led_matrix_panel(canvas, led_states) + + 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 diff --git a/test_ui.py b/test_ui.py new file mode 100644 index 0000000..c8afda0 --- /dev/null +++ b/test_ui.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +测试科技感UI界面 +""" + +import cv2 +import numpy as np +import sys +import os +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from src.ui.tech_ui import TechUI + + +class MockDetectionResult: + """模拟检测结果""" + def __init__(self): + self.frame_count = 1234 + self.processing_time = 0.045 + self.detection_mode = "normal" + self.detection_summary = { + 'threshold_detection': { + 'states': { + 'on': 12, + 'off': 6 + } + } + } + self.roi_detections = {} + + # 生成模拟的LED状态 + for row in range(1, 4): + for col in range(1, 7): + roi_name = f"R{row}C{col}" + is_on = (row * col) % 3 == 0 # 模拟一些LED开启 + self.roi_detections[roi_name] = { + 'threshold_detection': { + 'is_on': is_on + } + } + + +def main(): + """测试UI界面""" + print("测试科技感UI界面...") + + # 创建UI实例 + ui = TechUI() + + # 创建模拟视频帧 + video_frame = np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8) + + # 在视频帧上添加一些模拟的LED点 + for i in range(3): + for j in range(6): + x = 80 + j * 80 + y = 120 + i * 80 + color = (0, 255, 0) if (i * j) % 3 == 0 else (100, 100, 100) + cv2.circle(video_frame, (x, y), 15, color, -1) + cv2.putText(video_frame, f"R{i+1}C{j+1}", (x-15, y+30), + cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1) + + # 创建模拟检测结果 + detection_result = MockDetectionResult() + + # 生成显示帧 + display_frame = ui.create_display_frame(video_frame, detection_result) + + print("按任意键退出...") + cv2.imshow('烟台蓬莱国际机场低能见度识别软件', display_frame) + cv2.waitKey(0) + cv2.destroyAllWindows() + + print("UI测试完成!") + + +if __name__ == "__main__": + main() \ No newline at end of file