fix: 修复监控UI布局问题并添加退出按钮
This commit is contained in:
parent
a8afca7167
commit
c6a2e4ada0
241
main.py
241
main.py
@ -9,6 +9,7 @@ import argparse
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# 抑制OpenCV日志输出
|
||||
os.environ['OPENCV_LOG_LEVEL'] = 'ERROR'
|
||||
@ -22,9 +23,13 @@ else:
|
||||
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.utils.path_utils import resolve_app_path
|
||||
from src.utils.performance_limiter import PerformanceLimiter
|
||||
|
||||
from src.camera.opencv_camera import OpenCVCamera
|
||||
from src.preprocessing.image_enhancer import ImageEnhancer
|
||||
from src.roi_detection.led_detector import LEDDetector
|
||||
from src.roi_detection.threshold_detector import LEDState
|
||||
from src.output.result_formatter import ResultFormatter
|
||||
from src.output.logger import LEDLogger
|
||||
from src.output.alarm_manager import AlarmManager
|
||||
@ -37,14 +42,31 @@ class YantaiVisionXSystem:
|
||||
集成所有模块实现完整的LED检测流水线
|
||||
"""
|
||||
|
||||
def __init__(self, config_dir: str = "config"):
|
||||
def __init__(
|
||||
self,
|
||||
config_dir: str = "config",
|
||||
roi_config_path: Optional[str] = None,
|
||||
algorithm_config_path: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
初始化系统
|
||||
|
||||
Args:
|
||||
config_dir: 配置文件目录
|
||||
"""
|
||||
self.config_dir = Path(config_dir)
|
||||
self.config_dir = resolve_app_path(config_dir)
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._roi_config_path = (
|
||||
resolve_app_path(roi_config_path)
|
||||
if roi_config_path is not None
|
||||
else self.config_dir / "roi_config.yaml"
|
||||
)
|
||||
self._algorithm_config_path = (
|
||||
resolve_app_path(algorithm_config_path)
|
||||
if algorithm_config_path is not None
|
||||
else self.config_dir / "algorithm_config.yaml"
|
||||
)
|
||||
|
||||
# 初始化各个组件
|
||||
self.camera = None
|
||||
@ -54,11 +76,66 @@ class YantaiVisionXSystem:
|
||||
self.logger = LEDLogger()
|
||||
self.alarm_manager = AlarmManager(self.logger)
|
||||
self.tech_ui = TechUI()
|
||||
self.performance_limiter = PerformanceLimiter()
|
||||
|
||||
self._window_internal_name = "YantaiVisionX Monitor"
|
||||
self._window_display_name = "烟台蓬莱国际机场低能见度识别软件"
|
||||
self._window_created = False
|
||||
self._default_runtime_message = "运行结果:实时检测中"
|
||||
|
||||
# 状态变量
|
||||
self.is_running = False
|
||||
self.display_enabled = False
|
||||
self.runtime_message = self._default_runtime_message
|
||||
|
||||
def set_config_paths(
|
||||
self,
|
||||
roi_config_path: Optional[str] = None,
|
||||
algorithm_config_path: Optional[str] = None,
|
||||
) -> None:
|
||||
"""动态更新配置文件路径"""
|
||||
|
||||
if roi_config_path is not None:
|
||||
self._roi_config_path = resolve_app_path(roi_config_path)
|
||||
|
||||
if algorithm_config_path is not None:
|
||||
self._algorithm_config_path = resolve_app_path(algorithm_config_path)
|
||||
|
||||
def get_config_paths(self) -> dict:
|
||||
"""获取当前配置文件路径"""
|
||||
|
||||
return {
|
||||
"roi": str(self._roi_config_path),
|
||||
"algorithm": str(self._algorithm_config_path),
|
||||
}
|
||||
|
||||
def _configure_performance_limiter(self) -> None:
|
||||
"""根据算法配置更新性能限制。"""
|
||||
|
||||
performance_cfg = {}
|
||||
if self.led_detector and hasattr(self.led_detector, "algorithm_config"):
|
||||
performance_cfg = self.led_detector.algorithm_config.get("performance", {}) or {}
|
||||
|
||||
max_fps = performance_cfg.get("max_processing_fps")
|
||||
frame_stride = performance_cfg.get("frame_stride", 1)
|
||||
self.performance_limiter.update_settings(max_processing_fps=max_fps, frame_stride=frame_stride)
|
||||
self.logger.log_info(
|
||||
f"性能限制器配置: max_fps={self.performance_limiter.max_processing_fps}, "
|
||||
f"frame_stride={self.performance_limiter.frame_stride}"
|
||||
)
|
||||
|
||||
def update_performance_settings(
|
||||
self,
|
||||
max_processing_fps: Optional[float] = None,
|
||||
frame_stride: Optional[int] = None,
|
||||
) -> None:
|
||||
"""运行中动态调整性能限制参数"""
|
||||
|
||||
self.performance_limiter.update_settings(
|
||||
max_processing_fps=max_processing_fps,
|
||||
frame_stride=frame_stride,
|
||||
)
|
||||
|
||||
def initialize_system(self, camera_config=None) -> bool:
|
||||
"""
|
||||
初始化整个系统
|
||||
@ -77,15 +154,17 @@ class YantaiVisionXSystem:
|
||||
self.logger.log_info("图像增强器初始化完成")
|
||||
|
||||
# 2. 初始化LED检测器
|
||||
roi_config_path = self.config_dir / "roi_config.yaml"
|
||||
algorithm_config_path = self.config_dir / "algorithm_config.yaml"
|
||||
roi_config_path = self._roi_config_path
|
||||
algorithm_config_path = self._algorithm_config_path
|
||||
|
||||
self.led_detector = LEDDetector(
|
||||
str(roi_config_path),
|
||||
str(algorithm_config_path)
|
||||
roi_config_path,
|
||||
algorithm_config_path
|
||||
)
|
||||
self.logger.log_info("LED检测器初始化完成")
|
||||
|
||||
self._configure_performance_limiter()
|
||||
|
||||
if self.led_detector and hasattr(self.led_detector, 'algorithm_config'):
|
||||
self.alarm_manager.update_config(
|
||||
self.led_detector.algorithm_config.get('alarm')
|
||||
@ -124,6 +203,7 @@ class YantaiVisionXSystem:
|
||||
|
||||
self.display_enabled = display
|
||||
self.is_running = True
|
||||
self._reset_runtime_message()
|
||||
|
||||
# 打开摄像头
|
||||
if not self.camera.open():
|
||||
@ -132,6 +212,9 @@ class YantaiVisionXSystem:
|
||||
|
||||
self.logger.log_info("开始 LED 检测...")
|
||||
|
||||
limiter = self.performance_limiter
|
||||
frame_index = 0
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
# 读取帧
|
||||
@ -139,6 +222,11 @@ class YantaiVisionXSystem:
|
||||
if not success:
|
||||
self.logger.log_warning("读取帧失败")
|
||||
continue
|
||||
|
||||
current_frame_index = frame_index
|
||||
frame_index += 1
|
||||
if limiter and not limiter.should_process_frame(current_frame_index):
|
||||
continue
|
||||
|
||||
# 图像预处理
|
||||
enhanced_frame = self.image_enhancer.preprocess_frame(
|
||||
@ -173,6 +261,9 @@ class YantaiVisionXSystem:
|
||||
if self.display_enabled:
|
||||
self._display_results(frame, detection_result)
|
||||
|
||||
if limiter:
|
||||
limiter.enforce_rate_limit()
|
||||
|
||||
# 检查退出条件
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
@ -210,20 +301,33 @@ class YantaiVisionXSystem:
|
||||
|
||||
self.display_enabled = display
|
||||
self.is_running = True
|
||||
self._reset_runtime_message()
|
||||
|
||||
# 创建输出目录
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(exist_ok=True)
|
||||
|
||||
results = [] if self.logger.save_results_enabled else None
|
||||
last_frame = None
|
||||
last_detection_result = None
|
||||
|
||||
self.logger.log_info(f"开始处理视频: {video_path}")
|
||||
|
||||
limiter = self.performance_limiter
|
||||
frame_index = 0
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
success, frame = self.camera.read_frame()
|
||||
if not success:
|
||||
if self.display_enabled and last_frame is not None:
|
||||
self._freeze_last_frame(last_frame, last_detection_result)
|
||||
break # 视频结束
|
||||
|
||||
current_frame_index = frame_index
|
||||
frame_index += 1
|
||||
if limiter and not limiter.should_process_frame(current_frame_index):
|
||||
continue
|
||||
|
||||
# 图像预处理
|
||||
enhanced_frame = self.image_enhancer.preprocess_frame(
|
||||
@ -234,6 +338,11 @@ class YantaiVisionXSystem:
|
||||
detection_result = self.led_detector.detect_leds(enhanced_frame)
|
||||
if results is not None:
|
||||
results.append(detection_result)
|
||||
if self.display_enabled and frame is not None:
|
||||
last_frame = frame.copy()
|
||||
else:
|
||||
last_frame = None
|
||||
last_detection_result = detection_result
|
||||
|
||||
# 记录结果
|
||||
if detection_result.frame_count % 100 == 0:
|
||||
@ -254,6 +363,9 @@ class YantaiVisionXSystem:
|
||||
self._display_results(frame, detection_result)
|
||||
if cv2.waitKey(1) & 0xFF == ord('q'):
|
||||
break
|
||||
|
||||
if limiter:
|
||||
limiter.enforce_rate_limit()
|
||||
|
||||
# 保存批量结果
|
||||
if results:
|
||||
@ -290,6 +402,9 @@ class YantaiVisionXSystem:
|
||||
|
||||
return True
|
||||
|
||||
def _reset_runtime_message(self) -> None:
|
||||
self.runtime_message = self._default_runtime_message
|
||||
|
||||
def _display_results(self, original_frame, detection_result) -> None:
|
||||
"""
|
||||
显示检测结果
|
||||
@ -304,10 +419,15 @@ class YantaiVisionXSystem:
|
||||
)
|
||||
|
||||
# 使用科技感UI创建完整显示帧
|
||||
display_frame = self.tech_ui.create_display_frame(vis_frame, detection_result)
|
||||
display_frame = self.tech_ui.create_display_frame(
|
||||
vis_frame,
|
||||
detection_result,
|
||||
runtime_message=self.runtime_message,
|
||||
)
|
||||
|
||||
self._ensure_window()
|
||||
# 显示图像
|
||||
cv2.imshow('烟台蓬莱国际机场低能见度识别软件', display_frame)
|
||||
cv2.imshow(self._window_internal_name, display_frame)
|
||||
|
||||
# 在控制台显示矩阵状态(每10帧显示一次)
|
||||
if detection_result.frame_count % 10 == 0:
|
||||
@ -315,6 +435,63 @@ class YantaiVisionXSystem:
|
||||
self.formatter.format_to_matrix(detection_result)
|
||||
)
|
||||
print(f"\n{matrix_text}")
|
||||
|
||||
def _freeze_last_frame(self, frame, detection_result) -> None:
|
||||
"""视频播放结束后保持最后一帧并显示运行结果"""
|
||||
self.runtime_message = self._build_runtime_summary(detection_result)
|
||||
|
||||
if not self.display_enabled or frame is None:
|
||||
return
|
||||
|
||||
try:
|
||||
self._display_results(frame, detection_result)
|
||||
except Exception as exc:
|
||||
self.logger.log_warning(f"最后一帧渲染失败: {exc}")
|
||||
return
|
||||
|
||||
self.logger.log_info("视频播放结束,按Q或Esc退出最终画面")
|
||||
while True:
|
||||
key = cv2.waitKey(100) & 0xFF
|
||||
if key in (ord('q'), 27):
|
||||
break
|
||||
try:
|
||||
visible = cv2.getWindowProperty(self._window_internal_name, cv2.WND_PROP_VISIBLE)
|
||||
except Exception:
|
||||
visible = -1
|
||||
if visible < 1:
|
||||
break
|
||||
|
||||
def _build_runtime_summary(self, detection_result) -> str:
|
||||
if not detection_result:
|
||||
return "运行结果:未获取检测结果"
|
||||
|
||||
summary = getattr(detection_result, 'detection_summary', {}) or {}
|
||||
threshold_summary = summary.get('threshold_detection') or {}
|
||||
states = threshold_summary.get('states') or {}
|
||||
|
||||
counts = {
|
||||
'on': int(states.get('on', 0)),
|
||||
'off': int(states.get('off', 0)),
|
||||
'uncertain': int(states.get('uncertain', 0)),
|
||||
}
|
||||
|
||||
if counts['on'] + counts['off'] + counts['uncertain'] == 0:
|
||||
stable_states = getattr(detection_result, 'stable_states', {}) or {}
|
||||
for result in stable_states.values():
|
||||
state = getattr(result, 'led_state', None)
|
||||
if state == LEDState.ON:
|
||||
counts['on'] += 1
|
||||
elif state == LEDState.OFF:
|
||||
counts['off'] += 1
|
||||
else:
|
||||
counts['uncertain'] += 1
|
||||
|
||||
total = counts['on'] + counts['off'] + counts['uncertain']
|
||||
total_text = f"共{total}盏" if total else "总数未知"
|
||||
return (
|
||||
f"运行结果:亮{counts['on']} 灭{counts['off']} 不确定{counts['uncertain']}({total_text})"
|
||||
)
|
||||
|
||||
|
||||
def _cleanup(self) -> None:
|
||||
"""
|
||||
@ -327,10 +504,50 @@ class YantaiVisionXSystem:
|
||||
|
||||
if self.display_enabled:
|
||||
cv2.destroyAllWindows()
|
||||
self._window_created = False
|
||||
|
||||
self.logger.log_info("系统关闭完成")
|
||||
self.logger.close()
|
||||
|
||||
def _ensure_window(self) -> None:
|
||||
"""确保OpenCV窗口存在并设置标题"""
|
||||
if self._window_created or not self.display_enabled:
|
||||
return
|
||||
|
||||
cv2.namedWindow(self._window_internal_name, cv2.WINDOW_NORMAL)
|
||||
cv2.resizeWindow(
|
||||
self._window_internal_name,
|
||||
self.tech_ui.window_width,
|
||||
self.tech_ui.window_height,
|
||||
)
|
||||
cv2.setMouseCallback(self._window_internal_name, self._on_mouse_click)
|
||||
self._apply_window_title()
|
||||
self._window_created = True
|
||||
|
||||
def _on_mouse_click(self, event: int, x: int, y: int, flags: int, param) -> None:
|
||||
"""鼠标点击回调函数"""
|
||||
if event == cv2.EVENT_LBUTTONDOWN:
|
||||
if self.tech_ui.is_exit_button_clicked(x, y):
|
||||
self.logger.log_info("用户点击退出按钮")
|
||||
self.is_running = False
|
||||
|
||||
def _apply_window_title(self) -> None:
|
||||
"""在不同平台上设置窗口标题,优先保证中文显示正常"""
|
||||
display_title = self._window_display_name
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
|
||||
hwnd = ctypes.windll.user32.FindWindowW(None, self._window_internal_name)
|
||||
if hwnd:
|
||||
ctypes.windll.user32.SetWindowTextW(hwnd, display_title)
|
||||
else:
|
||||
cv2.setWindowTitle(self._window_internal_name, display_title)
|
||||
else:
|
||||
cv2.setWindowTitle(self._window_internal_name, display_title)
|
||||
except Exception:
|
||||
cv2.setWindowTitle(self._window_internal_name, display_title)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
@ -348,8 +565,16 @@ def main():
|
||||
help='显示实时检测结果')
|
||||
parser.add_argument('--config', type=str, default='config',
|
||||
help='配置文件目录路径')
|
||||
parser.add_argument('--ui', action='store_true',
|
||||
help='启动可视化控制面板')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.ui:
|
||||
from src.ui.control_panel import launch_control_panel
|
||||
|
||||
launch_control_panel()
|
||||
return 0
|
||||
|
||||
# 创建系统实例
|
||||
system = YantaiVisionXSystem(args.config)
|
||||
|
||||
@ -18,7 +18,14 @@ from src.roi_detection.threshold_detector import LEDState
|
||||
class TechUI:
|
||||
"""高级监控界面"""
|
||||
|
||||
def __init__(self, window_width: int = 1440, window_height: int = 900) -> None:
|
||||
def __init__(self, window_width: int = 1440, window_height: int = 920) -> None:
|
||||
# 基础画布尺寸(用于绘制后再统一缩放)
|
||||
self.base_width = 1600
|
||||
self.base_height = 1000
|
||||
|
||||
# 最终输出窗口尺寸(可小于或大于基础尺寸,实现等比例缩放)
|
||||
self.output_width = window_width
|
||||
self.output_height = window_height
|
||||
self.window_width = window_width
|
||||
self.window_height = window_height
|
||||
|
||||
@ -41,13 +48,14 @@ class TechUI:
|
||||
'chart_line': (120, 200, 255),
|
||||
}
|
||||
|
||||
right_col_x = self.base_width - 470 - 30
|
||||
self.panels = {
|
||||
'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),
|
||||
'header': (0, 0, self.base_width, 140),
|
||||
'video': (30, 150, 1050, 560),
|
||||
'timeline': (30, 720, 1050, 260),
|
||||
'status': (right_col_x, 150, 470, 250),
|
||||
'led_matrix': (right_col_x, 410, 470, 200),
|
||||
'stats': (right_col_x, 620, 470, 360),
|
||||
}
|
||||
|
||||
self.fonts = {
|
||||
@ -67,6 +75,9 @@ class TechUI:
|
||||
self.state_history: deque[float] = deque(maxlen=120)
|
||||
self.timeline_events: deque[Dict[str, Any]] = deque(maxlen=12)
|
||||
self.previous_states: Dict[str, LEDState] = {}
|
||||
|
||||
# 退出按钮区域 (将在draw_header中更新)
|
||||
self.exit_button_rect: Tuple[int, int, int, int] = (0, 0, 0, 0)
|
||||
|
||||
def _load_chinese_font(self) -> Optional[ImageFont.FreeTypeFont]:
|
||||
font_paths = [
|
||||
@ -127,7 +138,7 @@ class TechUI:
|
||||
|
||||
def create_main_canvas(self) -> np.ndarray:
|
||||
canvas = np.full(
|
||||
(self.window_height, self.window_width, 3),
|
||||
(self.base_height, self.base_width, 3),
|
||||
self.colors['bg_dark'],
|
||||
dtype=np.uint8,
|
||||
)
|
||||
@ -170,13 +181,22 @@ class TechUI:
|
||||
subtitle = "YantaiVisionX 状态控制台"
|
||||
self._put_chinese_text(canvas, subtitle, (x + 32, y + 68), 18, self.colors['text_secondary'])
|
||||
|
||||
# 退出按钮
|
||||
btn_w, btn_h = 70, 32
|
||||
btn_x = x + w - btn_w - 30
|
||||
btn_y = y + 12
|
||||
self.exit_button_rect = (btn_x, btn_y, btn_w, btn_h)
|
||||
cv2.rectangle(canvas, (btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h), self.colors['danger'], -1)
|
||||
cv2.rectangle(canvas, (btn_x, btn_y), (btn_x + btn_w, btn_y + btn_h), (100, 100, 180), 1)
|
||||
self._put_chinese_text(canvas, "退出", (btn_x + 16, btn_y + 4), 18, self.colors['text_primary'])
|
||||
|
||||
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),
|
||||
(x + w - time_size[0] - btn_w - 50, y + 44),
|
||||
self.fonts['mono'],
|
||||
time_scale,
|
||||
self.colors['text_secondary'],
|
||||
@ -249,11 +269,24 @@ class TechUI:
|
||||
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:
|
||||
def draw_status_panel(
|
||||
self,
|
||||
canvas: np.ndarray,
|
||||
detection_result,
|
||||
counts: Dict[str, int],
|
||||
runtime_message: Optional[str] = None,
|
||||
) -> None:
|
||||
x, y, w, h = self.panels['status']
|
||||
self._draw_glass_panel(canvas, x, y, w, h)
|
||||
self._draw_panel_header(canvas, x, y, "系统状态")
|
||||
|
||||
runtime_text = runtime_message or "运行结果:实时检测中"
|
||||
banner_x = x + 24
|
||||
banner_y = y + 58
|
||||
banner_w = w - 48
|
||||
banner_h = 50
|
||||
self._draw_runtime_banner(canvas, banner_x, banner_y, banner_w, banner_h, runtime_text)
|
||||
|
||||
fps = self._calc_fps(detection_result)
|
||||
latency = detection_result.processing_time * 1000 if detection_result else 0
|
||||
stability = (
|
||||
@ -261,30 +294,38 @@ class TechUI:
|
||||
if getattr(detection_result, 'detection_summary', None)
|
||||
else 0.0
|
||||
)
|
||||
grid_y = y + 60
|
||||
block_w = (w - 80) // 3
|
||||
grid_y = banner_y + banner_h + 16
|
||||
block_w = (w - 90) // 3
|
||||
block_h = 80
|
||||
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)
|
||||
block_x = x + 30 + idx * (block_w + 15)
|
||||
self._draw_metric_card(canvas, block_x, grid_y, block_w, block_h, label, value, unit, color)
|
||||
|
||||
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_count = getattr(detection_result, 'frame_count', 0)
|
||||
|
||||
footer_y = grid_y + block_h + 10
|
||||
footer_text = f"当前模式: {mode} 帧计数: {frame_count}"
|
||||
self._put_chinese_text(canvas, footer_text, (x + 30, footer_y), 15, self.colors['text_secondary'])
|
||||
|
||||
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._put_chinese_text(canvas, "LED状态矩阵", (x + 30, y + 18), 20, self.colors['text_primary'])
|
||||
summary_text = f"亮 {counts.get('on', 0)} | 灭 {counts.get('off', 0)} | 不确定 {counts.get('uncertain', 0)}"
|
||||
self._put_chinese_text(canvas, summary_text, (x + 180, y + 20), 14, self.colors['text_secondary'])
|
||||
|
||||
matrix_start_x = x + 40
|
||||
matrix_start_y = y + 80
|
||||
cell = 42
|
||||
gap = 14
|
||||
cell = 34
|
||||
gap = 8
|
||||
matrix_width = 6 * cell + 5 * gap
|
||||
matrix_start_x = x + (w - matrix_width) // 2
|
||||
matrix_start_y = y + 55
|
||||
color_map = {
|
||||
'on': self.colors['success'],
|
||||
'off': (80, 90, 120),
|
||||
@ -301,19 +342,20 @@ class TechUI:
|
||||
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,
|
||||
)
|
||||
|
||||
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'])
|
||||
label_y = matrix_start_y + 3 * (cell + gap) + 12
|
||||
for col in range(6):
|
||||
label_x = matrix_start_x + col * (cell + gap) + cell // 2 - 14
|
||||
cv2.putText(
|
||||
canvas,
|
||||
f"C{col + 1}",
|
||||
(label_x, label_y),
|
||||
self.fonts['mono'],
|
||||
0.4,
|
||||
self.colors['text_secondary'],
|
||||
1,
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
|
||||
def draw_statistics_panel(self, canvas: np.ndarray, detection_result, counts: Dict[str, int]) -> None:
|
||||
x, y, w, h = self.panels['stats']
|
||||
@ -324,15 +366,16 @@ class TechUI:
|
||||
on_ratio = counts.get('on', 0) / total
|
||||
off_ratio = counts.get('off', 0) / total
|
||||
|
||||
bar_height = 20
|
||||
first_bar_y = y + 70
|
||||
bar_height = 18
|
||||
first_bar_y = y + 80
|
||||
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'])
|
||||
|
||||
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)
|
||||
chart_top = second_bar_y + bar_height + 30
|
||||
chart_height = y + h - chart_top - 20
|
||||
if chart_height > 40:
|
||||
self._draw_trend_chart(canvas, x + 30, chart_top, w - 60, chart_height)
|
||||
|
||||
def draw_alarm_timeline(self, canvas: np.ndarray) -> None:
|
||||
x, y, w, h = self.panels['timeline']
|
||||
@ -399,6 +442,22 @@ class TechUI:
|
||||
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_runtime_banner(
|
||||
self,
|
||||
canvas: np.ndarray,
|
||||
x: int,
|
||||
y: int,
|
||||
w: int,
|
||||
h: int,
|
||||
text: str,
|
||||
) -> None:
|
||||
cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['panel_light'], -1,
|
||||
cv2.LINE_AA)
|
||||
cv2.rectangle(canvas, (x, y), (x + w, y + h), self.colors['accent_cyan'], 1,
|
||||
cv2.LINE_AA)
|
||||
self._put_chinese_text(canvas, text, (x + 14, y + h // 2 - 10), 20,
|
||||
self.colors['text_primary'])
|
||||
|
||||
def _draw_progress_bar(
|
||||
self,
|
||||
canvas: np.ndarray,
|
||||
@ -569,7 +628,12 @@ class TechUI:
|
||||
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:
|
||||
def create_display_frame(
|
||||
self,
|
||||
video_frame: np.ndarray,
|
||||
detection_result,
|
||||
runtime_message: Optional[str] = None,
|
||||
) -> np.ndarray:
|
||||
canvas = self.create_main_canvas()
|
||||
counts = self._extract_state_counts(detection_result)
|
||||
self._update_state_history(counts)
|
||||
@ -579,9 +643,34 @@ class TechUI:
|
||||
|
||||
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_status_panel(canvas, detection_result, counts, runtime_message)
|
||||
self.draw_led_matrix_panel(canvas, led_matrix, counts)
|
||||
self.draw_statistics_panel(canvas, detection_result, counts)
|
||||
self.draw_alarm_timeline(canvas)
|
||||
|
||||
return canvas
|
||||
if (self.output_width, self.output_height) != (self.base_width, self.base_height):
|
||||
interpolation = (
|
||||
cv2.INTER_AREA
|
||||
if self.output_width <= self.base_width and self.output_height <= self.base_height
|
||||
else cv2.INTER_LINEAR
|
||||
)
|
||||
canvas = cv2.resize(canvas, (self.output_width, self.output_height), interpolation=interpolation)
|
||||
|
||||
return canvas
|
||||
|
||||
def is_exit_button_clicked(self, click_x: int, click_y: int) -> bool:
|
||||
"""检测鼠标点击是否在退出按钮上(考虑缩放)"""
|
||||
if self.exit_button_rect == (0, 0, 0, 0):
|
||||
return False
|
||||
|
||||
scale_x = self.output_width / self.base_width
|
||||
scale_y = self.output_height / self.base_height
|
||||
|
||||
btn_x, btn_y, btn_w, btn_h = self.exit_button_rect
|
||||
scaled_x = int(btn_x * scale_x)
|
||||
scaled_y = int(btn_y * scale_y)
|
||||
scaled_w = int(btn_w * scale_x)
|
||||
scaled_h = int(btn_h * scale_y)
|
||||
|
||||
return (scaled_x <= click_x <= scaled_x + scaled_w and
|
||||
scaled_y <= click_y <= scaled_y + scaled_h)
|
||||
Loading…
Reference in New Issue
Block a user