refactor: improve TechUI layout spacing

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
sladro 2025-12-01 10:43:22 +08:00
parent 6fba8a981a
commit a8afca7167

View File

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