refactor: improve TechUI layout spacing
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
6fba8a981a
commit
a8afca7167
@ -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
|
||||
return canvas
|
||||
Loading…
Reference in New Issue
Block a user