feat: 添加科技感UI界面和中文标题支持

- 新增TechUI类,提供现代化科技感用户界面
- 支持中文标题显示:"烟台蓬莱国际机场低能见度识别软件"
- 集成多面板布局:视频显示、状态监控、LED矩阵、统计信息
- 添加PIL库支持中文字体渲染,解决乱码问题
- 更新main.py集成新UI界面,替换原简单显示
- 添加UI测试脚本test_ui.py用于界面效果预览
- 更新依赖项:添加Pillow和PyYAML支持

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-25 10:19:07 +08:00
parent 2ff9c2b0bb
commit a8e0bdcfe2
5 changed files with 588 additions and 25 deletions

39
main.py
View File

@ -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:

View File

@ -1,4 +1,6 @@
opencv-python>=4.8.0
numpy>=1.24.0
scikit-image>=0.20.0
matplotlib>=3.7.0
matplotlib>=3.7.0
Pillow>=9.5.0
PyYAML>=6.0

8
src/ui/__init__.py Normal file
View File

@ -0,0 +1,8 @@
"""
UI模块
为YantaiVisionX系统提供用户界面组件
"""
from .tech_ui import TechUI
__all__ = ['TechUI']

480
src/ui/tech_ui.py Normal file
View File

@ -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

82
test_ui.py Normal file
View File

@ -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()