
🎪 摸鱼匠:个人主页
🎒 个人专栏:《YOLOv11实战专栏》
🥇 没有好的理念,只有脚踏实地!

文章目录
-
-
-
- [6.4.5 全局错误处理器](#6.4.5 全局错误处理器)
- [6.4.6 系统状态监控](#6.4.6 系统状态监控)
- [6.4.7 主程序中的错误处理](#6.4.7 主程序中的错误处理)
- [6.5 用户界面设计](#6.5 用户界面设计)
-
- [6.5.1 基础UI组件](#6.5.1 基础UI组件)
- [6.5.2 控制面板实现](#6.5.2 控制面板实现)
- [6.5.3 状态面板实现](#6.5.3 状态面板实现)
- [6.5.4 增强的可视化器](#6.5.4 增强的可视化器)
- [6.5.5 集成UI到主程序](#6.5.5 集成UI到主程序)
-
- 七、部署与优化
-
- [7.1 系统性能测试](#7.1 系统性能测试)
-
- [7.1.1 性能测试框架](#7.1.1 性能测试框架)
-
6.4.5 全局错误处理器
我们实现一个全局错误处理器,用于捕获未处理的异常:
python
# src/utils/global_error_handler.py
import sys
import traceback
from typing import Optional
from src.utils.logger import logger
class GlobalErrorHandler:
"""
全局错误处理器
"""
def __init__(self):
"""初始化全局错误处理器"""
self.original_excepthook = sys.excepthook
sys.excepthook = self.handle_exception
def handle_exception(self, exc_type, exc_value, exc_traceback):
"""
处理未捕获的异常
参数:
exc_type: 异常类型
exc_value: 异常值
exc_traceback: 异常跟踪
"""
if issubclass(exc_type, KeyboardInterrupt):
# 允许KeyboardInterrupt正常退出
self.original_excepthook(exc_type, exc_value, exc_traceback)
return
# 记录异常
logger.critical("Unhandled exception:", exc_info=(exc_type, exc_value, exc_traceback))
# 调用原始异常处理器
self.original_excepthook(exc_type, exc_value, exc_traceback)
def restore(self):
"""恢复原始异常处理器"""
sys.excepthook = self.original_excepthook
# 全局错误处理器实例
global_error_handler = GlobalErrorHandler()
6.4.6 系统状态监控
我们实现一个系统状态监控器,用于监控系统运行状态并在出现问题时采取适当的行动:
python
# src/utils/system_monitor.py
import time
import threading
import psutil
from typing import Dict, Callable, Optional
from src.utils.logger import logger
class SystemMonitor:
"""
系统状态监控器
"""
def __init__(self, check_interval: float = 5.0):
"""
初始化系统监控器
参数:
check_interval: 检查间隔(秒)
"""
self.check_interval = check_interval
self.running = False
self.thread = None
# 监控阈值
self.cpu_threshold = 90.0 # CPU使用率阈值(%)
self.memory_threshold = 90.0 # 内存使用率阈值(%)
self.disk_threshold = 90.0 # 磁盘使用率阈值(%)
# 回调函数
self.callbacks = {
'high_cpu': [],
'high_memory': [],
'high_disk': [],
'error': []
}
# 状态历史
self.status_history = []
self.max_history = 100
def start(self) -> None:
"""启动监控"""
if self.running:
logger.warning("System monitor is already running")
return
self.running = True
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.thread.start()
logger.info("System monitor started")
def stop(self) -> None:
"""停止监控"""
if not self.running:
logger.warning("System monitor is not running")
return
self.running = False
if self.thread:
self.thread.join()
logger.info("System monitor stopped")
def _monitor_loop(self) -> None:
"""监控循环"""
while self.running:
try:
# 获取系统状态
status = self._get_system_status()
# 添加到历史记录
self.status_history.append(status)
if len(self.status_history) > self.max_history:
self.status_history.pop(0)
# 检查阈值
self._check_thresholds(status)
# 等待下一次检查
time.sleep(self.check_interval)
except Exception as e:
logger.error(f"Error in system monitor: {e}")
self._trigger_callbacks('error', {'error': str(e)})
time.sleep(self.check_interval)
def _get_system_status(self) -> Dict:
"""获取系统状态"""
# CPU使用率
cpu_percent = psutil.cpu_percent(interval=1)
# 内存使用率
memory = psutil.virtual_memory()
memory_percent = memory.percent
# 磁盘使用率
disk = psutil.disk_usage('/')
disk_percent = disk.percent
return {
'timestamp': time.time(),
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'disk_percent': disk_percent,
'memory_available': memory.available,
'disk_free': disk.free
}
def _check_thresholds(self, status: Dict) -> None:
"""检查阈值并触发回调"""
# 检查CPU使用率
if status['cpu_percent'] > self.cpu_threshold:
self._trigger_callbacks('high_cpu', status)
# 检查内存使用率
if status['memory_percent'] > self.memory_threshold:
self._trigger_callbacks('high_memory', status)
# 检查磁盘使用率
if status['disk_percent'] > self.disk_threshold:
self._trigger_callbacks('high_disk', status)
def _trigger_callbacks(self, event_type: str, data: Dict) -> None:
"""触发回调函数"""
if event_type in self.callbacks:
for callback in self.callbacks[event_type]:
try:
callback(data)
except Exception as e:
logger.error(f"Error in callback for {event_type}: {e}")
def add_callback(self, event_type: str, callback: Callable[[Dict], None]) -> None:
"""
添加回调函数
参数:
event_type: 事件类型
callback: 回调函数
"""
if event_type in self.callbacks:
self.callbacks[event_type].append(callback)
else:
logger.warning(f"Unknown event type: {event_type}")
def remove_callback(self, event_type: str, callback: Callable[[Dict], None]) -> None:
"""
移除回调函数
参数:
event_type: 事件类型
callback: 回调函数
"""
if event_type in self.callbacks and callback in self.callbacks[event_type]:
self.callbacks[event_type].remove(callback)
else:
logger.warning(f"Callback not found for event type: {event_type}")
def get_status_history(self, count: Optional[int] = None) -> list:
"""
获取状态历史
参数:
count: 返回的记录数,None表示全部
返回:
状态历史列表
"""
if count is None:
return self.status_history.copy()
else:
return self.status_history[-count:]
def get_current_status(self) -> Optional[Dict]:
"""
获取当前状态
返回:
当前状态字典
"""
if self.status_history:
return self.status_history[-1].copy()
return None
# 全局系统监控器实例
system_monitor = SystemMonitor()
6.4.7 主程序中的错误处理
最后,我们在主程序中集成错误处理和日志系统:
python
# src/main.py(添加错误处理的部分)
import os
import sys
import argparse
import time
import signal
from typing import Optional
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.config_manager import config_manager
from src.detection.yolo_detector import YOLODetector
from src.tracking.multi_tracker import MultiObjectTracker
from src.zones.zone_manager import ZoneManager
from src.alerts.alert_system import AlertSystem
from src.recording.detection_recorder import DetectionRecorder
from src.visualization.result_visualizer import ResultVisualizer
from src.utils.camera_utils import setup_camera, test_camera
from src.utils.logger import logger
from src.utils.global_error_handler import global_error_handler
from src.utils.system_monitor import system_monitor
from src.utils.error_handling import handle_exceptions
from src.utils.exceptions import DetectionSystemError, CameraError, ModelLoadError
class DetectionApp:
"""
检测应用程序主类(带错误处理)
"""
def __init__(self, config_path: Optional[str] = None):
"""
初始化检测应用
参数:
config_path: 自定义配置文件路径
"""
# 加载配置
if config_path:
config_manager.load_config(config_path)
# 初始化组件
self.detector = None
self.tracker = None
self.zone_manager = None
self.alert_system = None
self.recorder = None
self.visualizer = None
self.camera = None
self.performance_monitor = None
# 运行状态
self.running = False
self.error_count = 0
self.max_errors = 10 # 最大错误次数
# 注册信号处理器
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, self.signal_handler)
# 添加系统监控回调
system_monitor.add_callback('high_cpu', self._handle_high_cpu)
system_monitor.add_callback('high_memory', self._handle_high_memory)
system_monitor.add_callback('high_disk', self._handle_high_disk)
system_monitor.add_callback('error', self._handle_system_error)
@handle_exceptions(exception_types=(DetectionSystemError, CameraError, ModelLoadError),
default_return=False, reraise=False)
def initialize(self) -> bool:
"""
初始化所有组件(带错误处理)
返回:
是否成功初始化
"""
try:
logger.info("Initializing detection system...")
# 启动系统监控
system_monitor.start()
# 初始化性能监控
self.performance_monitor = PerformanceMonitor()
# 初始化摄像头
camera_index = config_manager.get('camera.index', 0)
camera_width = config_manager.get('camera.width', 640)
camera_height = config_manager.get('camera.height', 480)
camera_fps = config_manager.get('camera.fps', 30)
self.camera = setup_camera(
index=camera_index,
width=camera_width,
height=camera_height,
fps=camera_fps
)
if not self.camera:
raise CameraError("Failed to initialize camera")
# 测试摄像头
if not test_camera(self.camera, duration=2):
logger.warning("Camera test failed, but continuing...")
# 初始化检测器
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = config_manager.get('model.input_size', 640)
conf_threshold = config_manager.get('model.conf_threshold', 0.5)
iou_threshold = config_manager.get('model.iou_threshold', 0.45)
device = config_manager.get('model.device', 'auto')
self.detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
device=device
)
# 初始化跟踪器
if config_manager.get('tracking.enabled', True):
max_age = config_manager.get('tracking.max_age', 40)
min_hits = config_manager.get('tracking.min_hits', 3)
iou_threshold = config_manager.get('tracking.iou_threshold', 0.3)
self.tracker = MultiObjectTracker(
max_age=max_age,
min_hits=min_hits,
iou_threshold=iou_threshold
)
# 初始化区域管理器
if config_manager.get('zones.enabled', True):
zones_file = config_manager.get('zones.file_path', 'data/zones/default_zones.json')
self.zone_manager = ZoneManager((camera_height, camera_width))
if not self.zone_manager.load_zones(zones_file):
logger.warning("No zones file found, zones will be empty")
# 初始化报警系统
if config_manager.get('alerts.enabled', True):
rules_file = config_manager.get('alerts.file_path', 'data/alert_rules/default_rules.json')
self.alert_system = AlertSystem(rules_file)
# 初始化记录器
if config_manager.get('recording.enabled', True):
output_dir = config_manager.get('recording.output_dir', 'outputs/recordings')
self.recorder = DetectionRecorder(output_dir=output_dir)
# 初始化可视化器
self.visualizer = ResultVisualizer()
logger.info("Detection system initialized successfully")
return True
except Exception as e:
logger.error(f"Error initializing detection system: {e}")
return False
@handle_exceptions(exception_types=DetectionSystemError,
default_return=None, reraise=False)
def run(self) -> None:
"""运行检测系统(带错误处理)"""
if not self.initialize():
logger.error("Failed to initialize detection system")
return
logger.info("Starting detection system...")
self.running = True
# 主循环
try:
while self.running:
try:
# 记录开始时间
start_time = time.time()
# 从摄像头读取帧
ret, frame = self.camera.read()
if not ret:
logger.error("Failed to read frame from camera")
self.error_count += 1
# 尝试重新初始化摄像头
if self.error_count >= 3:
logger.warning("Attempting to reinitialize camera...")
self.camera.release()
camera_index = config_manager.get('camera.index', 0)
self.camera = setup_camera(index=camera_index)
self.error_count = 0
if self.error_count >= self.max_errors:
logger.error(f"Too many errors ({self.error_count}), stopping...")
break
continue
# 重置错误计数
self.error_count = 0
# 更新性能监控
self.performance_monitor.update()
# 运行检测
detections = self.detector.detect(frame)
# 运行跟踪
tracked_objects = None
if self.tracker:
dets_for_tracking = [
[det['bbox'][0], det['bbox'][1], det['bbox'][2]-det['bbox'][0], det['bbox'][3]-det['bbox'][1], det['confidence']]
for det in detections
]
tracked_objects = self.tracker.update(dets_for_tracking)
# 区域过滤
if self.zone_manager and config_manager.get('zones.filter_enabled', True):
detections, _ = self.zone_manager.filter_detections_by_zones(detections)
# 检查报警
if self.alert_system:
self.alert_system.check_alerts(detections, tracked_objects, self.zone_manager, frame)
# 记录检测结果
if self.recorder:
self.recorder.record_detections(self.performance_monitor.frame_count, detections, tracked_objects)
# 可视化结果
result_frame = self.visualizer.visualize(
frame,
detections,
tracked_objects,
self.zone_manager,
self.performance_monitor
)
# 显示结果
cv2.imshow('YOLOv11 Detection', result_frame)
# 处理按键
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
self.running = False
elif key == ord('z') and self.zone_manager:
# 编辑区域
saved = self.zone_manager.edit_zones(frame)
if saved:
zones_file = config_manager.get('zones.file_path')
self.zone_manager.save_zones(zones_file)
elif key == ord('a') and self.alert_system:
# 切换报警系统
config_manager.set('alerts.enabled', not config_manager.get('alerts.enabled'))
logger.info(f"Alerts {'enabled' if config_manager.get('alerts.enabled') else 'disabled'}")
# 控制帧率
max_fps = config_manager.get('detection.max_fps', 30)
if max_fps > 0:
elapsed = time.time() - start_time
delay = max(1, int((1/max_fps - elapsed) * 1000))
cv2.waitKey(delay)
except Exception as e:
logger.error(f"Error in main loop: {e}")
self.error_count += 1
if self.error_count >= self.max_errors:
logger.error(f"Too many errors ({self.error_count}), stopping...")
break
except KeyboardInterrupt:
logger.info("Detection system interrupted by user")
except Exception as e:
logger.error(f"Unexpected error in detection system: {e}")
finally:
self.cleanup()
def cleanup(self) -> None:
"""清理资源"""
logger.info("Cleaning up resources...")
# 停止系统监控
system_monitor.stop()
if self.camera:
self.camera.release()
if self.recorder:
self.recorder.save_statistics_to_db()
self.recorder.export_to_csv()
self.recorder.generate_report()
cv2.destroyAllWindows()
logger.info("Detection system stopped")
def signal_handler(self, signum, frame):
"""信号处理器"""
logger.info(f"Received signal {signum}, stopping...")
self.running = False
def _handle_high_cpu(self, data):
"""处理高CPU使用率"""
logger.warning(f"High CPU usage: {data['cpu_percent']:.1f}%")
# 可以采取一些缓解措施,如降低检测频率
current_max_fps = config_manager.get('detection.max_fps', 30)
if current_max_fps > 5:
new_max_fps = max(5, current_max_fps - 5)
config_manager.set('detection.max_fps', new_max_fps)
logger.info(f"Reduced max FPS to {new_max_fps} due to high CPU usage")
def _handle_high_memory(self, data):
"""处理高内存使用率"""
logger.warning(f"High memory usage: {data['memory_percent']:.1f}%")
# 可以采取一些缓解措施,如减少历史记录长度
if self.tracker:
# 这里可以添加减少跟踪历史长度的逻辑
pass
def _handle_high_disk(self, data):
"""处理高磁盘使用率"""
logger.warning(f"High disk usage: {data['disk_percent']:.1f}%")
# 可以采取一些缓解措施,如清理旧日志文件
if self.recorder:
# 这里可以添加清理旧记录的逻辑
pass
def _handle_system_error(self, data):
"""处理系统错误"""
logger.error(f"System error: {data['error']}")
def parse_arguments():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description='YOLOv11 Real-Time Detection System')
parser.add_argument('--config', type=str, help='Path to config file')
parser.add_argument('--model', type=str, help='Path to model file')
parser.add_argument('--camera', type=int, help='Camera index')
parser.add_argument('--no-tracking', action='store_true', help='Disable tracking')
parser.add_argument('--no-zones', action='store_true', help='Disable zones')
parser.add_argument('--no-alerts', action='store_true', help='Disable alerts')
parser.add_argument('--no-recording', action='store_true', help='Disable recording')
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
default='INFO', help='Set logging level')
return parser.parse_args()
def main():
"""主函数"""
# 解析命令行参数
args = parse_arguments()
# 设置日志级别
logger.logger.setLevel(args.log_level)
# 更新配置
if args.config:
config_manager.load_config(args.config)
if args.model:
config_manager.set('model.path', args.model)
if args.camera is not None:
config_manager.set('camera.index', args.camera)
if args.no_tracking:
config_manager.set('tracking.enabled', False)
if args.no_zones:
config_manager.set('zones.enabled', False)
if args.no_alerts:
config_manager.set('alerts.enabled', False)
if args.no_recording:
config_manager.set('recording.enabled', False)
# 创建并运行检测应用
try:
app = DetectionApp()
app.run()
except Exception as e:
logger.critical(f"Fatal error in main: {e}")
global_error_handler.restore()
raise
if __name__ == "__main__":
main()
通过实现这些错误处理和日志记录机制,我们的系统现在能够:
- 优雅地处理错误:捕获并记录各种异常,防止系统崩溃。
- 提供详细的日志信息:记录系统运行状态和错误信息,便于调试和监控。
- 自动恢复:在某些错误情况下尝试自动恢复,如重新初始化摄像头。
- 监控系统资源:监控CPU、内存和磁盘使用率,并在超过阈值时采取相应措施。
- 记录性能统计:跟踪系统性能指标,帮助优化系统。
这些机制大大提高了系统的健壮性和可维护性,使其能够在生产环境中稳定运行。
6.5 用户界面设计
用户界面(UI)是实时目标检测系统与用户交互的重要部分。一个好的UI不仅能够直观地展示检测结果,还能提供便捷的控制选项,使用户能够轻松地配置和操作系统。本节将介绍如何为我们的YOLOv11实时检测系统设计一个功能丰富且用户友好的界面。
6.5.1 基础UI组件
首先,我们实现一些基础的UI组件,用于显示信息、按钮和控件:
python
# src/visualization/ui_components.py
import cv2
import numpy as np
from typing import List, Tuple, Dict, Optional, Callable
from src.config_manager import config_manager
from src.utils.logger import logger
class UIComponent:
"""UI组件基类"""
def __init__(self, x: int, y: int, width: int, height: int):
"""
初始化UI组件
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.visible = True
self.enabled = True
self.hover = False
self.pressed = False
def contains_point(self, x: int, y: int) -> bool:
"""
检查点是否在组件内
参数:
x: X坐标
y: Y坐标
返回:
点是否在组件内
"""
return (self.x <= x <= self.x + self.width and
self.y <= y <= self.y + self.height)
def handle_mouse_event(self, event: int, x: int, y: int, flags: int) -> bool:
"""
处理鼠标事件
参数:
event: 事件类型
x: X坐标
y: Y坐标
flags: 事件标志
返回:
是否处理了事件
"""
if not self.visible or not self.enabled:
return False
if event == cv2.EVENT_MOUSEMOVE:
self.hover = self.contains_point(x, y)
return self.hover
elif event == cv2.EVENT_LBUTTONDOWN:
if self.contains_point(x, y):
self.pressed = True
return True
elif event == cv2.EVENT_LBUTTONUP:
if self.pressed and self.contains_point(x, y):
self.pressed = False
self.on_click()
return True
self.pressed = False
return False
def on_click(self):
"""点击事件处理"""
pass
def draw(self, image: np.ndarray) -> None:
"""
绘制组件
参数:
image: 要绘制的图像
"""
pass
class Button(UIComponent):
"""按钮组件"""
def __init__(self, x: int, y: int, width: int, height: int, text: str,
callback: Optional[Callable] = None, bg_color: Tuple[int, int, int] = (50, 50, 50),
text_color: Tuple[int, int, int] = (255, 255, 255)):
"""
初始化按钮
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
text: 按钮文本
callback: 点击回调函数
bg_color: 背景颜色
text_color: 文本颜色
"""
super().__init__(x, y, width, height)
self.text = text
self.callback = callback
self.bg_color = bg_color
self.text_color = text_color
self.hover_color = tuple(min(255, c + 30) for c in bg_color)
self.pressed_color = tuple(max(0, c - 30) for c in bg_color)
def on_click(self):
"""点击事件处理"""
if self.callback:
self.callback()
def draw(self, image: np.ndarray) -> None:
"""绘制按钮"""
if not self.visible:
return
# 确定颜色
if self.pressed:
color = self.pressed_color
elif self.hover:
color = self.hover_color
else:
color = self.bg_color
# 绘制按钮背景
cv2.rectangle(image, (self.x, self.y),
(self.x + self.width, self.y + self.height),
color, -1)
# 绘制按钮边框
cv2.rectangle(image, (self.x, self.y),
(self.x + self.width, self.y + self.height),
(200, 200, 200), 1)
# 绘制文本
font_scale = 0.5
text_size = cv2.getTextSize(self.text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.x + (self.width - text_size[0]) // 2
text_y = self.y + (self.height + text_size[1]) // 2
cv2.putText(image, self.text, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, self.text_color, 1)
class CheckBox(UIComponent):
"""复选框组件"""
def __init__(self, x: int, y: int, size: int, text: str,
checked: bool = False, callback: Optional[Callable[[bool], None]] = None):
"""
初始化复选框
参数:
x: X坐标
y: Y坐标
size: 复选框大小
text: 复选框文本
checked: 是否选中
callback: 状态改变回调函数
"""
super().__init__(x, y, size, size)
self.text = text
self.checked = checked
self.callback = callback
self.box_color = (200, 200, 200)
self.check_color = (0, 255, 0)
self.text_color = (255, 255, 255)
def on_click(self):
"""点击事件处理"""
self.checked = not self.checked
if self.callback:
self.callback(self.checked)
def draw(self, image: np.ndarray) -> None:
"""绘制复选框"""
if not self.visible:
return
# 绘制复选框
cv2.rectangle(image, (self.x, self.y),
(self.x + self.width, self.y + self.height),
self.box_color, 2)
# 如果选中,绘制勾
if self.checked:
pt1 = (self.x + 5, self.y + self.height // 2)
pt2 = (self.x + self.width // 2 - 2, self.y + self.height - 5)
pt3 = (self.x + self.width - 5, self.y + 5)
cv2.line(image, pt1, pt2, self.check_color, 2)
cv2.line(image, pt2, pt3, self.check_color, 2)
# 绘制文本
font_scale = 0.5
text_size = cv2.getTextSize(self.text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.x + self.width + 10
text_y = self.y + (self.height + text_size[1]) // 2
cv2.putText(image, self.text, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, self.text_color, 1)
class Slider(UIComponent):
"""滑块组件"""
def __init__(self, x: int, y: int, width: int, height: int,
min_value: float = 0.0, max_value: float = 1.0,
initial_value: float = 0.5, text: str = "",
callback: Optional[Callable[[float], None]] = None):
"""
初始化滑块
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
min_value: 最小值
max_value: 最大值
initial_value: 初始值
text: 滑块文本
callback: 值改变回调函数
"""
super().__init__(x, y, width, height)
self.min_value = min_value
self.max_value = max_value
self.value = initial_value
self.text = text
self.callback = callback
self.dragging = False
# 计算滑块位置
self._update_handle_position()
def _update_handle_position(self):
"""更新滑块位置"""
ratio = (self.value - self.min_value) / (self.max_value - self.min_value)
self.handle_x = self.x + int(ratio * (self.width - 20)) + 10
def handle_mouse_event(self, event: int, x: int, y: int, flags: int) -> bool:
"""处理鼠标事件"""
if not self.visible or not self.enabled:
return False
if event == cv2.EVENT_LBUTTONDOWN:
# 检查是否点击了滑块手柄
handle_rect = (self.handle_x - 5, self.y - 5,
self.handle_x + 5, self.y + self.height + 5)
if (handle_rect[0] <= x <= handle_rect[2] and
handle_rect[1] <= y <= handle_rect[3]):
self.dragging = True
return True
elif event == cv2.EVENT_MOUSEMOVE:
if self.dragging:
# 更新滑块值
new_x = max(self.x + 10, min(x, self.x + self.width - 10))
ratio = (new_x - self.x - 10) / (self.width - 20)
self.value = self.min_value + ratio * (self.max_value - self.min_value)
self._update_handle_position()
if self.callback:
self.callback(self.value)
return True
elif event == cv2.EVENT_LBUTTONUP:
if self.dragging:
self.dragging = False
return True
return super().handle_mouse_event(event, x, y, flags)
def draw(self, image: np.ndarray) -> None:
"""绘制滑块"""
if not self.visible:
return
# 绘制滑块轨道
cv2.rectangle(image, (self.x + 10, self.y + self.height // 2 - 2),
(self.x + self.width - 10, self.y + self.height // 2 + 2),
(100, 100, 100), -1)
# 绘制滑块手柄
cv2.rectangle(image, (self.handle_x - 5, self.y - 5),
(self.handle_x + 5, self.y + self.height + 5),
(200, 200, 200), -1)
# 绘制文本
if self.text:
font_scale = 0.5
text_size = cv2.getTextSize(self.text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.x + self.width // 2 - text_size[0] // 2
text_y = self.y - 10
cv2.putText(image, self.text, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), 1)
# 绘制值
value_text = f"{self.value:.2f}"
font_scale = 0.4
text_size = cv2.getTextSize(value_text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.x + self.width + 10
text_y = self.y + self.height // 2 + text_size[1] // 2
cv2.putText(image, value_text, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), 1)
class Panel:
"""面板容器"""
def __init__(self, x: int, y: int, width: int, height: int,
title: str = "", bg_color: Tuple[int, int, int] = (40, 40, 40),
title_color: Tuple[int, int, int] = (255, 255, 255)):
"""
初始化面板
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
title: 面板标题
bg_color: 背景颜色
title_color: 标题颜色
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.title = title
self.bg_color = bg_color
self.title_color = title_color
self.visible = True
self.components = []
def add_component(self, component: UIComponent) -> None:
"""
添加组件到面板
参数:
component: 要添加的组件
"""
self.components.append(component)
def handle_mouse_event(self, event: int, x: int, y: int, flags: int) -> bool:
"""
处理鼠标事件
参数:
event: 事件类型
x: X坐标
y: Y坐标
flags: 事件标志
返回:
是否处理了事件
"""
if not self.visible:
return False
# 从上到下处理组件(后添加的在上层)
for component in reversed(self.components):
if component.handle_mouse_event(event, x, y, flags):
return True
return False
def draw(self, image: np.ndarray) -> None:
"""
绘制面板和所有组件
参数:
image: 要绘制的图像
"""
if not self.visible:
return
# 绘制面板背景
cv2.rectangle(image, (self.x, self.y),
(self.x + self.width, self.y + self.height),
self.bg_color, -1)
# 绘制面板边框
cv2.rectangle(image, (self.x, self.y),
(self.x + self.width, self.y + self.height),
(100, 100, 100), 1)
# 绘制标题
if self.title:
font_scale = 0.6
text_size = cv2.getTextSize(self.title, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.x + (self.width - text_size[0]) // 2
text_y = self.y + 25
cv2.putText(image, self.title, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, self.title_color, 1)
# 绘制标题下划线
cv2.line(image, (self.x + 20, text_y + 10),
(self.x + self.width - 20, text_y + 10),
(100, 100, 100), 1)
# 绘制所有组件
for component in self.components:
component.draw(image)
6.5.2 控制面板实现
接下来,我们实现一个控制面板,包含各种控制选项,如开关、滑块和按钮:
python
# src/visualization/control_panel.py
import cv2
import numpy as np
from typing import Dict, List, Optional
from src.visualization.ui_components import Panel, Button, CheckBox, Slider
from src.config_manager import config_manager
from src.utils.logger import logger
class ControlPanel:
"""
控制面板类
"""
def __init__(self, x: int, y: int, width: int, height: int):
"""
初始化控制面板
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
"""
self.panel = Panel(x, y, width, height, "Control Panel")
self.buttons = {}
self.checkboxes = {}
self.sliders = {}
# 初始化UI组件
self._init_components()
def _init_components(self) -> None:
"""初始化UI组件"""
# 添加按钮
self._add_button("save_config", 10, 40, 100, 30, "Save Config", self._save_config)
self._add_button("load_config", 120, 40, 100, 30, "Load Config", self._load_config)
self._add_button("edit_zones", 230, 40, 100, 30, "Edit Zones", self._edit_zones)
self._add_button("reset_stats", 10, 80, 100, 30, "Reset Stats", self._reset_stats)
self._add_button("export_data", 120, 80, 100, 30, "Export Data", self._export_data)
# 添加复选框
self._add_checkbox("show_fps", 10, 130, 20, "Show FPS",
config_manager.get('visualization.show_fps', True),
self._toggle_show_fps)
self._add_checkbox("show_confidence", 10, 160, 20, "Show Confidence",
config_manager.get('visualization.show_confidence', True),
self._toggle_show_confidence)
self._add_checkbox("show_tracking", 10, 190, 20, "Show Tracking",
config_manager.get('visualization.show_tracking', True),
self._toggle_show_tracking)
self._add_checkbox("show_zones", 10, 220, 20, "Show Zones",
config_manager.get('visualization.show_zones', True),
self._toggle_show_zones)
self._add_checkbox("enable_alerts", 10, 250, 20, "Enable Alerts",
config_manager.get('alerts.enabled', True),
self._toggle_alerts)
self._add_checkbox("enable_recording", 10, 280, 20, "Enable Recording",
config_manager.get('recording.enabled', True),
self._toggle_recording)
# 添加滑块
self._add_slider("conf_threshold", 10, 330, 200, 20,
0.1, 1.0, config_manager.get('model.conf_threshold', 0.5),
"Confidence", self._update_conf_threshold)
self._add_slider("iou_threshold", 10, 370, 200, 20,
0.1, 1.0, config_manager.get('model.iou_threshold', 0.45),
"IOU Threshold", self._update_iou_threshold)
self._add_slider("max_fps", 10, 410, 200, 20,
1, 60, config_manager.get('detection.max_fps', 30),
"Max FPS", self._update_max_fps)
def _add_button(self, name: str, x: int, y: int, width: int, height: int,
text: str, callback) -> None:
"""
添加按钮
参数:
name: 按钮名称
x: X坐标
y: Y坐标
width: 宽度
height: 高度
text: 按钮文本
callback: 点击回调函数
"""
button = Button(x, y, width, height, text, callback)
self.buttons[name] = button
self.panel.add_component(button)
def _add_checkbox(self, name: str, x: int, y: int, size: int, text: str,
checked: bool, callback) -> None:
"""
添加复选框
参数:
name: 复选框名称
x: X坐标
y: Y坐标
size: 大小
text: 复选框文本
checked: 是否选中
callback: 状态改变回调函数
"""
checkbox = CheckBox(x, y, size, text, checked, callback)
self.checkboxes[name] = checkbox
self.panel.add_component(checkbox)
def _add_slider(self, name: str, x: int, y: int, width: int, height: int,
min_value: float, max_value: float, initial_value: float,
text: str, callback) -> None:
"""
添加滑块
参数:
name: 滑块名称
x: X坐标
y: Y坐标
width: 宽度
height: 高度
min_value: 最小值
max_value: 最大值
initial_value: 初始值
text: 滑块文本
callback: 值改变回调函数
"""
slider = Slider(x, y, width, height, min_value, max_value, initial_value, text, callback)
self.sliders[name] = slider
self.panel.add_component(slider)
# 按钮回调函数
def _save_config(self) -> None:
"""保存配置"""
config_manager.save_config()
logger.info("Configuration saved")
def _load_config(self) -> None:
"""加载配置"""
config_manager.load_config()
self._update_components_from_config()
logger.info("Configuration loaded")
def _edit_zones(self) -> None:
"""编辑区域"""
# 这个函数需要从外部设置
if hasattr(self, 'edit_zones_callback'):
self.edit_zones_callback()
def _reset_stats(self) -> None:
"""重置统计"""
# 这个函数需要从外部设置
if hasattr(self, 'reset_stats_callback'):
self.reset_stats_callback()
def _export_data(self) -> None:
"""导出数据"""
# 这个函数需要从外部设置
if hasattr(self, 'export_data_callback'):
self.export_data_callback()
# 复选框回调函数
def _toggle_show_fps(self, checked: bool) -> None:
"""切换显示FPS"""
config_manager.set('visualization.show_fps', checked)
def _toggle_show_confidence(self, checked: bool) -> None:
"""切换显示置信度"""
config_manager.set('visualization.show_confidence', checked)
def _toggle_show_tracking(self, checked: bool) -> None:
"""切换显示跟踪"""
config_manager.set('visualization.show_tracking', checked)
def _toggle_show_zones(self, checked: bool) -> None:
"""切换显示区域"""
config_manager.set('visualization.show_zones', checked)
def _toggle_alerts(self, checked: bool) -> None:
"""切换报警"""
config_manager.set('alerts.enabled', checked)
def _toggle_recording(self, checked: bool) -> None:
"""切换记录"""
config_manager.set('recording.enabled', checked)
# 滑块回调函数
def _update_conf_threshold(self, value: float) -> None:
"""更新置信度阈值"""
config_manager.set('model.conf_threshold', value)
def _update_iou_threshold(self, value: float) -> None:
"""更新IOU阈值"""
config_manager.set('model.iou_threshold', value)
def _update_max_fps(self, value: float) -> None:
"""更新最大FPS"""
config_manager.set('detection.max_fps', int(value))
def _update_components_from_config(self) -> None:
"""从配置更新组件状态"""
# 更新复选框
for name, checkbox in self.checkboxes.items():
config_key = f"visualization.{name}" if name.startswith("show_") else f"{name}.enabled"
checkbox.checked = config_manager.get(config_key, False)
# 更新滑块
conf_threshold = self.sliders.get("conf_threshold")
if conf_threshold:
conf_threshold.value = config_manager.get('model.conf_threshold', 0.5)
conf_threshold._update_handle_position()
iou_threshold = self.sliders.get("iou_threshold")
if iou_threshold:
iou_threshold.value = config_manager.get('model.iou_threshold', 0.45)
iou_threshold._update_handle_position()
max_fps = self.sliders.get("max_fps")
if max_fps:
max_fps.value = config_manager.get('detection.max_fps', 30)
max_fps._update_handle_position()
def handle_mouse_event(self, event: int, x: int, y: int, flags: int) -> bool:
"""
处理鼠标事件
参数:
event: 事件类型
x: X坐标
y: Y坐标
flags: 事件标志
返回:
是否处理了事件
"""
return self.panel.handle_mouse_event(event, x, y, flags)
def draw(self, image: np.ndarray) -> None:
"""
绘制控制面板
参数:
image: 要绘制的图像
"""
self.panel.draw(image)
def set_edit_zones_callback(self, callback) -> None:
"""
设置编辑区域回调函数
参数:
callback: 回调函数
"""
self.edit_zones_callback = callback
def set_reset_stats_callback(self, callback) -> None:
"""
设置重置统计回调函数
参数:
callback: 回调函数
"""
self.reset_stats_callback = callback
def set_export_data_callback(self, callback) -> None:
"""
设置导出数据回调函数
参数:
callback: 回调函数
"""
self.export_data_callback = callback
6.5.3 状态面板实现
我们实现一个状态面板,用于显示系统状态和性能信息:
python
# src/visualization/status_panel.py
import cv2
import numpy as np
import time
from typing import Dict, List, Optional
from src.visualization.ui_components import Panel
from src.config_manager import config_manager
from src.utils.logger import logger
class StatusPanel:
"""
状态面板类
"""
def __init__(self, x: int, y: int, width: int, height: int):
"""
初始化状态面板
参数:
x: X坐标
y: Y坐标
width: 宽度
height: 高度
"""
self.panel = Panel(x, y, width, height, "System Status")
# 状态信息
self.fps = 0
self.inference_time = 0
self.detection_count = 0
self.tracking_count = 0
self.alert_count = 0
self.frame_count = 0
self.start_time = time.time()
# 性能历史
self.fps_history = []
self.max_history = 100
def update_status(self, fps: float, inference_time: float,
detection_count: int, tracking_count: int,
alert_count: int, frame_count: int) -> None:
"""
更新状态信息
参数:
fps: 帧率
inference_time: 推理时间
detection_count: 检测数量
tracking_count: 跟踪数量
alert_count: 报警数量
frame_count: 帧计数
"""
self.fps = fps
self.inference_time = inference_time
self.detection_count = detection_count
self.tracking_count = tracking_count
self.alert_count = alert_count
self.frame_count = frame_count
# 更新FPS历史
self.fps_history.append(fps)
if len(self.fps_history) > self.max_history:
self.fps_history.pop(0)
def draw(self, image: np.ndarray) -> None:
"""
绘制状态面板
参数:
image: 要绘制的图像
"""
if not self.panel.visible:
return
# 绘制面板背景
cv2.rectangle(image, (self.panel.x, self.panel.y),
(self.panel.x + self.panel.width, self.panel.y + self.panel.height),
self.panel.bg_color, -1)
# 绘制面板边框
cv2.rectangle(image, (self.panel.x, self.panel.y),
(self.panel.x + self.panel.width, self.panel.y + self.panel.height),
(100, 100, 100), 1)
# 绘制标题
if self.panel.title:
font_scale = 0.6
text_size = cv2.getTextSize(self.panel.title, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0]
text_x = self.panel.x + (self.panel.width - text_size[0]) // 2
text_y = self.panel.y + 25
cv2.putText(image, self.panel.title, (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, self.panel.title_color, 1)
# 绘制标题下划线
cv2.line(image, (self.panel.x + 20, text_y + 10),
(self.panel.x + self.panel.width - 20, text_y + 10),
(100, 100, 100), 1)
# 绘制状态信息
y_offset = 60
line_height = 25
font_scale = 0.5
text_color = (200, 200, 200)
# FPS
fps_text = f"FPS: {self.fps:.1f}"
cv2.putText(image, fps_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 推理时间
inference_text = f"Inference: {self.inference_time:.1f}ms"
cv2.putText(image, inference_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 检测数量
detection_text = f"Detections: {self.detection_count}"
cv2.putText(image, detection_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 跟踪数量
tracking_text = f"Tracking: {self.tracking_count}"
cv2.putText(image, tracking_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 报警数量
alert_text = f"Alerts: {self.alert_count}"
cv2.putText(image, alert_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 运行时间
elapsed_time = time.time() - self.start_time
hours = int(elapsed_time // 3600)
minutes = int((elapsed_time % 3600) // 60)
seconds = int(elapsed_time % 60)
time_text = f"Runtime: {hours:02d}:{minutes:02d}:{seconds:02d}"
cv2.putText(image, time_text, (self.panel.x + 10, self.panel.y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, text_color, 1)
y_offset += line_height
# 绘制FPS图表
if len(self.fps_history) > 1:
self._draw_fps_graph(image, y_offset + 10)
def _draw_fps_graph(self, image: np.ndarray, y_offset: int) -> None:
"""
绘制FPS图表
参数:
image: 要绘制的图像
y_offset: Y偏移量
"""
# 图表参数
graph_x = self.panel.x + 10
graph_y = self.panel.y + y_offset
graph_width = self.panel.width - 20
graph_height = 80
# 绘制图表背景
cv2.rectangle(image, (graph_x, graph_y),
(graph_x + graph_width, graph_y + graph_height),
(30, 30, 30), -1)
# 绘制图表边框
cv2.rectangle(image, (graph_x, graph_y),
(graph_x + graph_width, graph_y + graph_height),
(100, 100, 100), 1)
# 绘制标题
title_text = "FPS History"
title_size = cv2.getTextSize(title_text, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)[0]
title_x = graph_x + (graph_width - title_size[0]) // 2
cv2.putText(image, title_text, (title_x, graph_y - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
# 绘制FPS曲线
if len(self.fps_history) > 1:
# 计算点的位置
points = []
max_fps = max(self.fps_history) if self.fps_history else 1
for i, fps in enumerate(self.fps_history):
x = graph_x + int(i * graph_width / len(self.fps_history))
y = graph_y + graph_height - int(fps * graph_height / max_fps)
points.append((x, y))
# 绘制曲线
for i in range(1, len(points)):
cv2.line(image, points[i-1], points[i], (0, 255, 0), 1)
# 绘制当前FPS值
current_fps = self.fps_history[-1]
fps_text = f"{current_fps:.1f}"
cv2.putText(image, fps_text, (points[-1][0] + 5, points[-1][1]),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1)
6.5.4 增强的可视化器
最后,我们增强结果可视化器,集成UI面板并处理用户交互:
python
# src/visualization/result_visualizer.py(增强版)
import cv2
import numpy as np
import time
from typing import List, Dict, Optional, Tuple
from src.config_manager import config_manager
from src.visualization.ui_components import UIComponent
from src.visualization.control_panel import ControlPanel
from src.visualization.status_panel import StatusPanel
from src.utils.logger import logger
class ResultVisualizer:
"""
结果可视化器(增强版)
"""
def __init__(self):
"""初始化结果可视化器"""
# 初始化UI面板
self.control_panel = None
self.status_panel = None
self.ui_initialized = False
# 鼠标事件处理
self.mouse_callback = None
# 最近报警
self.recent_alerts = []
self.max_recent_alerts = 5
def initialize_ui(self, image_shape: Tuple[int, int]) -> None:
"""
初始化UI组件
参数:
image_shape: 图像形状 (height, width)
"""
if self.ui_initialized:
return
height, width = image_shape
# 创建控制面板
panel_width = 250
panel_height = height - 20
self.control_panel = ControlPanel(width - panel_width - 10, 10, panel_width, panel_height)
# 创建状态面板
self.status_panel = StatusPanel(10, 10, 200, 200)
# 设置鼠标回调
self.mouse_callback = lambda event, x, y, flags, param: self._handle_mouse_event(event, x, y, flags)
cv2.setMouseCallback(config_manager.get('ui.window_name', 'YOLOv11 Detection'), self.mouse_callback)
self.ui_initialized = True
logger.info("UI components initialized")
def _handle_mouse_event(self, event: int, x: int, y: int, flags: int) -> None:
"""
处理鼠标事件
参数:
event: 事件类型
x: X坐标
y: Y坐标
flags: 事件标志
"""
# 先检查控制面板
if self.control_panel and self.control_panel.handle_mouse_event(event, x, y, flags):
return
# 再检查状态面板
if self.status_panel and self.status_panel.handle_mouse_event(event, x, y, flags):
return
def visualize(self, frame: np.ndarray, detections: List[Dict],
tracked_objects: Optional[np.ndarray] = None,
zone_manager = None,
performance_monitor = None) -> np.ndarray:
"""
可视化检测结果
参数:
frame: 输入帧
detections: 检测结果列表
tracked_objects: 跟踪对象数组
zone_manager: 区域管理器
performance_monitor: 性能监控器
返回:
可视化结果帧
"""
# 初始化UI(如果尚未初始化)
if not self.ui_initialized:
self.initialize_ui(frame.shape[:2])
# 复制帧
result_frame = frame.copy()
# 绘制检测结果
if config_manager.get('visualization.show_confidence', True):
result_frame = self._draw_detections(result_frame, detections)
# 绘制跟踪结果
if config_manager.get('visualization.show_tracking', True) and tracked_objects is not None:
result_frame = self._draw_tracking(result_frame, tracked_objects)
# 绘制区域
if config_manager.get('visualization.show_zones', True) and zone_manager is not None:
result_frame = zone_manager.draw_zones(result_frame)
# 绘制最近报警
if self.recent_alerts:
result_frame = self._draw_recent_alerts(result_frame)
# 绘制性能信息
if config_manager.get('visualization.show_fps', True) and performance_monitor is not None:
result_frame = self._draw_performance_info(result_frame, performance_monitor)
# 更新状态面板
if self.status_panel and performance_monitor:
self.status_panel.update_status(
performance_monitor.fps,
performance_monitor.inference_time * 1000, # 转换为毫秒
len(detections),
len(tracked_objects) if tracked_objects is not None else 0,
len(self.recent_alerts),
performance_monitor.frame_count
)
# 绘制UI面板
if self.control_panel:
self.control_panel.draw(result_frame)
if self.status_panel:
self.status_panel.draw(result_frame)
return result_frame
def _draw_detections(self, image: np.ndarray, detections: List[Dict]) -> np.ndarray:
"""
绘制检测结果
参数:
image: 输入图像
detections: 检测结果列表
返回:
绘制结果图像
"""
bbox_thickness = config_manager.get('visualization.bbox_thickness', 2)
font_scale = config_manager.get('visualization.font_scale', 0.7)
for detection in detections:
# 获取边界框和类别信息
x1, y1, x2, y2 = detection['bbox']
class_name = detection['class_name']
confidence = detection['confidence']
# 绘制边界框
cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), bbox_thickness)
# 准备标签文本
label = f"{class_name}: {confidence:.2f}"
# 计算文本尺寸
(label_width, label_height), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)
# 绘制标签背景
cv2.rectangle(image, (x1, y1 - label_height - baseline),
(x1 + label_width, y1), (0, 255, 0), -1)
# 绘制标签文本
cv2.putText(image, label, (x1, y1 - baseline),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, (0, 0, 0), 1)
return image
def _draw_tracking(self, image: np.ndarray, tracked_objects: np.ndarray) -> np.ndarray:
"""
绘制跟踪结果
参数:
image: 输入图像
tracked_objects: 跟踪对象数组
返回:
绘制结果图像
"""
if tracked_objects.size == 0:
return image
font_scale = config_manager.get('visualization.font_scale', 0.7)
# 为每个跟踪ID分配颜色
track_ids = tracked_objects[:, 4].astype(int)
unique_ids = np.unique(track_ids)
colors = {track_id: (np.random.randint(0, 255), np.random.randint(0, 255), np.random.randint(0, 255))
for track_id in unique_ids}
# 绘制每个跟踪对象
for obj in tracked_objects:
x1, y1, x2, y2, track_id = obj.astype(int)
color = colors.get(track_id, (0, 255, 0))
# 绘制边界框
cv2.rectangle(image, (x1, y1), (x2, y2), color, 2)
# 绘制跟踪ID
cv2.putText(image, f"ID {track_id}", (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, 2)
return image
def _draw_recent_alerts(self, image: np.ndarray) -> np.ndarray:
"""
绘制最近报警
参数:
image: 输入图像
返回:
绘制结果图像
"""
if not self.recent_alerts:
return image
# 绘制报警背景
alert_bg_height = min(len(self.recent_alerts) * 25 + 40, 200)
cv2.rectangle(image,
(image.shape[1] - 310, image.shape[1] - 10),
(image.shape[1] - 10, alert_bg_height),
(0, 0, 0), -1)
cv2.rectangle(image,
(image.shape[1] - 310, image.shape[1] - 10),
(image.shape[1] - 10, alert_bg_height),
(0, 0, 255), 1)
# 显示报警标题
cv2.putText(image, "Recent Alerts",
(image.shape[1] - 300, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 1)
# 显示报警信息
for i, alert in enumerate(self.recent_alerts[-5:]): # 只显示最近5条
y_pos = 55 + i * 25
text = f"{alert['timestamp']} - {alert['rule_name']}"
cv2.putText(image, text,
(image.shape[1] - 300, y_pos),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)
return image
def _draw_performance_info(self, image: np.ndarray, performance_monitor) -> np.ndarray:
"""
绘制性能信息
参数:
image: 输入图像
performance_monitor: 性能监控器
返回:
绘制结果图像
"""
# 绘制性能信息背景
cv2.rectangle(image, (10, image.shape[0] - 100), (250, image.shape[0] - 10), (0, 0, 0), -1)
cv2.rectangle(image, (10, image.shape[0] - 100), (250, image.shape[0] - 10), (100, 100, 100), 1)
# 显示性能信息
info_text = [
f"FPS: {performance_monitor.fps:.1f}",
f"Inference: {performance_monitor.inference_time*1000:.1f}ms",
f"Preprocess: {performance_monitor.preprocess_time*1000:.1f}ms",
f"Postprocess: {performance_monitor.postprocess_time*1000:.1f}ms"
]
for i, text in enumerate(info_text):
cv2.putText(image, text, (20, image.shape[0] - 70 + i * 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 1)
return image
def add_alert(self, rule_name: str, trigger_info: Dict) -> None:
"""
添加报警信息
参数:
rule_name: 规则名称
trigger_info: 触发信息
"""
from datetime import datetime
alert = {
'timestamp': datetime.now().strftime('%H:%M:%S'),
'rule_name': rule_name,
'trigger_info': trigger_info
}
self.recent_alerts.append(alert)
# 限制最近报警列表大小
if len(self.recent_alerts) > self.max_recent_alerts:
self.recent_alerts.pop(0)
def set_edit_zones_callback(self, callback) -> None:
"""
设置编辑区域回调函数
参数:
callback: 回调函数
"""
if self.control_panel:
self.control_panel.set_edit_zones_callback(callback)
def set_reset_stats_callback(self, callback) -> None:
"""
设置重置统计回调函数
参数:
callback: 回调函数
"""
if self.control_panel:
self.control_panel.set_reset_stats_callback(callback)
def set_export_data_callback(self, callback) -> None:
"""
设置导出数据回调函数
参数:
callback: 回调函数
"""
if self.control_panel:
self.control_panel.set_export_data_callback(callback)
6.5.5 集成UI到主程序
最后,我们将UI集成到主程序中:
python
# src/main.py(集成UI的部分)
# ... 其他导入 ...
from src.visualization.result_visualizer import ResultVisualizer
class DetectionApp:
"""
检测应用程序主类(集成UI)
"""
# ... 其他方法 ...
def initialize(self) -> bool:
"""
初始化所有组件(集成UI)
返回:
是否成功初始化
"""
try:
logger.info("Initializing detection system...")
# 启动系统监控
system_monitor.start()
# 初始化性能监控
self.performance_monitor = PerformanceMonitor()
# 初始化摄像头
camera_index = config_manager.get('camera.index', 0)
camera_width = config_manager.get('camera.width', 640)
camera_height = config_manager.get('camera.height', 480)
camera_fps = config_manager.get('camera.fps', 30)
self.camera = setup_camera(
index=camera_index,
width=camera_width,
height=camera_height,
fps=camera_fps
)
if not self.camera:
raise CameraError("Failed to initialize camera")
# 测试摄像头
if not test_camera(self.camera, duration=2):
logger.warning("Camera test failed, but continuing...")
# 初始化检测器
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = config_manager.get('model.input_size', 640)
conf_threshold = config_manager.get('model.conf_threshold', 0.5)
iou_threshold = config_manager.get('model.iou_threshold', 0.45)
device = config_manager.get('model.device', 'auto')
self.detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
device=device
)
# 初始化跟踪器
if config_manager.get('tracking.enabled', True):
max_age = config_manager.get('tracking.max_age', 40)
min_hits = config_manager.get('tracking.min_hits', 3)
iou_threshold = config_manager.get('tracking.iou_threshold', 0.3)
self.tracker = MultiObjectTracker(
max_age=max_age,
min_hits=min_hits,
iou_threshold=iou_threshold
)
# 初始化区域管理器
if config_manager.get('zones.enabled', True):
zones_file = config_manager.get('zones.file_path', 'data/zones/default_zones.json')
self.zone_manager = ZoneManager((camera_height, camera_width))
if not self.zone_manager.load_zones(zones_file):
logger.warning("No zones file found, zones will be empty")
# 初始化报警系统
if config_manager.get('alerts.enabled', True):
rules_file = config_manager.get('alerts.file_path', 'data/alert_rules/default_rules.json')
self.alert_system = AlertSystem(rules_file)
# 初始化记录器
if config_manager.get('recording.enabled', True):
output_dir = config_manager.get('recording.output_dir', 'outputs/recordings')
self.recorder = DetectionRecorder(output_dir=output_dir)
# 初始化可视化器
self.visualizer = ResultVisualizer()
# 设置UI回调
self._setup_ui_callbacks()
logger.info("Detection system initialized successfully")
return True
except Exception as e:
logger.error(f"Error initializing detection system: {e}")
return False
def _setup_ui_callbacks(self) -> None:
"""设置UI回调函数"""
if self.visualizer:
self.visualizer.set_edit_zones_callback(self._edit_zones)
self.visualizer.set_reset_stats_callback(self._reset_stats)
self.visualizer.set_export_data_callback(self._export_data)
def _edit_zones(self) -> None:
"""编辑区域"""
if self.zone_manager:
# 获取一帧用于编辑
ret, frame = self.camera.read()
if ret:
saved = self.zone_manager.edit_zones(frame)
if saved:
zones_file = config_manager.get('zones.file_path')
self.zone_manager.save_zones(zones_file)
def _reset_stats(self) -> None:
"""重置统计"""
if self.performance_monitor:
self.performance_monitor.reset()
if self.recorder:
self.recorder.stats = {
'total_frames': 0,
'total_detections': 0,
'class_counts': {},
'hourly_counts': {},
'daily_counts': {}
}
logger.info("Statistics reset")
def _export_data(self) -> None:
"""导出数据"""
if self.recorder:
self.recorder.save_statistics_to_db()
self.recorder.export_to_csv()
self.recorder.export_statistics_to_json()
self.recorder.generate_report()
logger.info("Data exported")
# ... 其他方法 ...
def run(self) -> None:
"""运行检测系统(集成UI)"""
if not self.initialize():
logger.error("Failed to initialize detection system")
return
logger.info("Starting detection system...")
self.running = True
# 设置窗口
window_name = config_manager.get('ui.window_name', 'YOLOv11 Detection')
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)
# 全屏模式
if config_manager.get('ui.fullscreen', False):
cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
# 主循环
try:
while self.running:
try:
# 记录开始时间
start_time = time.time()
# 从摄像头读取帧
ret, frame = self.camera.read()
if not ret:
logger.error("Failed to read frame from camera")
self.error_count += 1
# 尝试重新初始化摄像头
if self.error_count >= 3:
logger.warning("Attempting to reinitialize camera...")
self.camera.release()
camera_index = config_manager.get('camera.index', 0)
self.camera = setup_camera(index=camera_index)
self.error_count = 0
if self.error_count >= self.max_errors:
logger.error(f"Too many errors ({self.error_count}), stopping...")
break
continue
# 重置错误计数
self.error_count = 0
# 更新性能监控
self.performance_monitor.update()
# 运行检测
detections = self.detector.detect(frame)
# 运行跟踪
tracked_objects = None
if self.tracker:
dets_for_tracking = [
[det['bbox'][0], det['bbox'][1], det['bbox'][2]-det['bbox'][0], det['bbox'][3]-det['bbox'][1], det['confidence']]
for det in detections
]
tracked_objects = self.tracker.update(dets_for_tracking)
# 区域过滤
if self.zone_manager and config_manager.get('zones.filter_enabled', True):
detections, _ = self.zone_manager.filter_detections_by_zones(detections)
# 检查报警
if self.alert_system:
triggered_rules = self.alert_system.check_alerts(detections, tracked_objects, self.zone_manager, frame)
# 更新最近报警
for rule, trigger_info in triggered_rules:
self.visualizer.add_alert(rule.name, trigger_info)
# 记录检测结果
if self.recorder:
self.recorder.record_detections(self.performance_monitor.frame_count, detections, tracked_objects)
# 可视化结果
result_frame = self.visualizer.visualize(
frame,
detections,
tracked_objects,
self.zone_manager,
self.performance_monitor
)
# 显示结果
cv2.imshow(window_name, result_frame)
# 处理按键
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
self.running = False
elif key == ord('f'):
# 切换全屏模式
current_mode = cv2.getWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN)
new_mode = cv2.WINDOW_FULLSCREEN if current_mode != cv2.WINDOW_FULLSCREEN else cv2.WINDOW_NORMAL
cv2.setWindowProperty(window_name, cv2.WND_PROP_FULLSCREEN, new_mode)
config_manager.set('ui.fullscreen', new_mode == cv2.WINDOW_FULLSCREEN)
elif key == ord('h'):
# 切换控制面板可见性
if self.visualizer.control_panel:
self.visualizer.control_panel.panel.visible = not self.visualizer.control_panel.panel.visible
elif key == ord('s'):
# 切换状态面板可见性
if self.visualizer.status_panel:
self.visualizer.status_panel.panel.visible = not self.visualizer.status_panel.panel.visible
# 控制帧率
max_fps = config_manager.get('detection.max_fps', 30)
if max_fps > 0:
elapsed = time.time() - start_time
delay = max(1, int((1/max_fps - elapsed) * 1000))
cv2.waitKey(delay)
except Exception as e:
logger.error(f"Error in main loop: {e}")
self.error_count += 1
if self.error_count >= self.max_errors:
logger.error(f"Too many errors ({self.error_count}), stopping...")
break
except KeyboardInterrupt:
logger.info("Detection system interrupted by user")
except Exception as e:
logger.error(f"Unexpected error in detection system: {e}")
finally:
self.cleanup()
# ... 其他方法 ...
# ... 其他代码 ...
通过实现这些UI组件和面板,我们为YOLOv11实时检测系统创建了一个功能丰富且用户友好的界面。用户现在可以:
- 实时查看检测结果:包括边界框、类别标签和置信度。
- 监控系统性能:通过状态面板查看FPS、推理时间等性能指标。
- 调整检测参数:通过控制面板实时调整置信度阈值、IOU阈值等参数。
- 控制功能开关:启用/禁用跟踪、区域过滤、报警和记录功能。
- 查看报警信息:在界面上直接查看最近的报警信息。
- 管理配置:保存和加载配置文件。
- 编辑检测区域:通过UI编辑检测区域。
- 导出数据:通过UI导出检测记录和统计信息。
这个UI设计不仅提供了丰富的功能,还保持了良好的用户体验,使用户能够轻松地监控和控制实时检测系统。
七、部署与优化
7.1 系统性能测试
在部署YOLOv11与OpenCV联动的实时目标检测系统之前,进行全面的性能测试是至关重要的。性能测试不仅可以帮助我们了解系统的能力和限制,还可以识别性能瓶颈,指导我们进行有针对性的优化。本节将介绍如何设计和执行系统性能测试,以及如何分析和解释测试结果。
7.1.1 性能测试框架
首先,我们实现一个性能测试框架,用于自动化执行各种测试并收集结果:
python
# tests/performance_test.py
import time
import psutil
import threading
import json
import os
from typing import Dict, List, Callable, Any, Optional
import numpy as np
import cv2
from src.config_manager import config_manager
from src.detection.yolo_detector import YOLODetector
from src.tracking.multi_tracker import MultiObjectTracker
from src.utils.logger import logger
from src.utils.camera_utils import setup_camera
class PerformanceTest:
"""
性能测试类
"""
def __init__(self, output_dir: str = "test_results"):
"""
初始化性能测试
参数:
output_dir: 输出目录
"""
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
# 测试结果
self.results = {}
# 系统监控
self.monitoring = False
self.monitor_thread = None
self.system_stats = []
# 测试配置
self.test_configs = {
'model_sizes': ['yolov11n', 'yolov11s', 'yolov11m', 'yolov11l', 'yolov11x'],
'input_sizes': [320, 416, 512, 640, 832],
'batch_sizes': [1, 2, 4],
'devices': ['cpu', 'cuda'],
'conf_thresholds': [0.3, 0.5, 0.7],
'iou_thresholds': [0.3, 0.5, 0.7]
}
def run_all_tests(self) -> Dict[str, Any]:
"""
运行所有性能测试
返回:
测试结果字典
"""
logger.info("Starting comprehensive performance tests...")
# 运行基础性能测试
self.results['basic_performance'] = self.test_basic_performance()
# 运行模型大小测试
self.results['model_size_comparison'] = self.test_model_size_comparison()
# 运行输入大小测试
self.results['input_size_comparison'] = self.test_input_size_comparison()
# 运行批处理测试
self.results['batch_size_comparison'] = self.test_batch_size_comparison()
# 运行设备比较测试
self.results['device_comparison'] = self.test_device_comparison()
# 运行参数敏感性测试
self.results['parameter_sensitivity'] = self.test_parameter_sensitivity()
# 运行长期稳定性测试
self.results['stability_test'] = self.test_stability(duration=300) # 5分钟
# 保存结果
self.save_results()
logger.info("Performance tests completed")
return self.results
def test_basic_performance(self) -> Dict[str, Any]:
"""
基础性能测试
返回:
测试结果字典
"""
logger.info("Running basic performance test...")
# 初始化组件
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = config_manager.get('model.input_size', 640)
conf_threshold = config_manager.get('model.conf_threshold', 0.5)
iou_threshold = config_manager.get('model.iou_threshold', 0.45)
device = config_manager.get('model.device', 'auto')
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
device=device
)
# 准备测试图像
test_image = self._generate_test_image(input_size, input_size)
# 预热
for _ in range(10):
detector.detect(test_image)
# 测试推理性能
inference_times = []
preprocess_times = []
postprocess_times = []
for _ in range(100):
start_time = time.time()
detections = detector.detect(test_image)
end_time = time.time()
inference_times.append(detector.inference_time * 1000) # 转换为毫秒
preprocess_times.append(detector.preprocess_time * 1000)
postprocess_times.append(detector.postprocess_time * 1000)
# 计算统计信息
result = {
'model': config_manager.get('model.name', 'yolov11n'),
'input_size': input_size,
'device': device,
'inference': {
'mean': np.mean(inference_times),
'std': np.std(inference_times),
'min': np.min(inference_times),
'max': np.max(inference_times),
'median': np.median(inference_times),
'p95': np.percentile(inference_times, 95)
},
'preprocess': {
'mean': np.mean(preprocess_times),
'std': np.std(preprocess_times),
'min': np.min(preprocess_times),
'max': np.max(preprocess_times)
},
'postprocess': {
'mean': np.mean(postprocess_times),
'std': np.std(postprocess_times),
'min': np.min(postprocess_times),
'max': np.max(postprocess_times)
},
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean([len(detector.detect(test_image)) for _ in range(10)])
}
logger.info(f"Basic performance test completed: {result['fps']:.2f} FPS")
return result
def test_model_size_comparison(self) -> List[Dict[str, Any]]:
"""
模型大小比较测试
返回:
测试结果列表
"""
logger.info("Running model size comparison test...")
results = []
input_size = 640
test_image = self._generate_test_image(input_size, input_size)
for model_size in self.test_configs['model_sizes']:
logger.info(f"Testing model: {model_size}")
try:
# 初始化检测器
model_path = f"models/{model_size}.pt"
if not os.path.exists(model_path):
logger.warning(f"Model file not found: {model_path}, skipping...")
continue
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=0.45,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
for _ in range(50):
detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
# 计算统计信息
result = {
'model': model_size,
'input_size': input_size,
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean([len(detector.detect(test_image)) for _ in range(5)])
}
results.append(result)
logger.info(f"Model {model_size}: {result['fps']:.2f} FPS")
except Exception as e:
logger.error(f"Error testing model {model_size}: {e}")
return results
def test_input_size_comparison(self) -> List[Dict[str, Any]]:
"""
输入大小比较测试
返回:
测试结果列表
"""
logger.info("Running input size comparison test...")
results = []
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
for input_size in self.test_configs['input_sizes']:
```python
logger.info(f"Testing input size: {input_size}")
try:
# 初始化检测器
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=0.45,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
# 准备测试图像
test_image = self._generate_test_image(input_size, input_size)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
for _ in range(50):
detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
# 计算统计信息
result = {
'input_size': input_size,
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean([len(detector.detect(test_image)) for _ in range(5)])
}
results.append(result)
logger.info(f"Input size {input_size}: {result['fps']:.2f} FPS")
except Exception as e:
logger.error(f"Error testing input size {input_size}: {e}")
return results
def test_batch_size_comparison(self) -> List[Dict[str, Any]]:
"""
批处理大小比较测试
返回:
测试结果列表
"""
logger.info("Running batch size comparison test...")
results = []
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = 640
for batch_size in self.test_configs['batch_sizes']:
logger.info(f"Testing batch size: {batch_size}")
try:
# 初始化检测器
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=0.45,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
# 准备测试图像批次
test_images = [self._generate_test_image(input_size, input_size) for _ in range(batch_size)]
# 预热
for _ in range(5):
detector.detect_batch(test_images)
# 测试推理性能
inference_times = []
for _ in range(50):
detector.detect_batch(test_images)
inference_times.append(detector.inference_time * 1000)
# 计算统计信息
result = {
'batch_size': batch_size,
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps_per_image': batch_size * 1000 / np.mean(inference_times),
'avg_detections_per_image': np.mean([len(detector.detect(test_images[0])) for _ in range(5)])
}
results.append(result)
logger.info(f"Batch size {batch_size}: {result['fps_per_image']:.2f} FPS per image")
except Exception as e:
logger.error(f"Error testing batch size {batch_size}: {e}")
return results
def test_device_comparison(self) -> List[Dict[str, Any]]:
"""
设备比较测试
返回:
测试结果列表
"""
logger.info("Running device comparison test...")
results = []
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = 640
test_image = self._generate_test_image(input_size, input_size)
# 测试CPU
if 'cpu' in self.test_configs['devices']:
logger.info("Testing CPU device...")
try:
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=0.45,
device='cpu'
)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
for _ in range(50):
detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
# 计算统计信息
result = {
'device': 'cpu',
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean([len(detector.detect(test_image)) for _ in range(5)])
}
results.append(result)
logger.info(f"CPU: {result['fps']:.2f} FPS")
except Exception as e:
logger.error(f"Error testing CPU device: {e}")
# 测试CUDA
if 'cuda' in self.test_configs['devices'] and torch.cuda.is_available():
logger.info("Testing CUDA device...")
try:
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=0.45,
device='cuda'
)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
for _ in range(50):
detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
# 计算统计信息
result = {
'device': 'cuda',
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean([len(detector.detect(test_image)) for _ in range(5)])
}
results.append(result)
logger.info(f"CUDA: {result['fps']:.2f} FPS")
except Exception as e:
logger.error(f"Error testing CUDA device: {e}")
return results
def test_parameter_sensitivity(self) -> Dict[str, Any]:
"""
参数敏感性测试
返回:
测试结果字典
"""
logger.info("Running parameter sensitivity test...")
results = {}
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = 640
test_image = self._generate_test_image(input_size, input_size)
# 测试置信度阈值
conf_results = []
for conf_threshold in self.test_configs['conf_thresholds']:
logger.info(f"Testing confidence threshold: {conf_threshold}")
try:
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=conf_threshold,
iou_threshold=0.45,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
detection_counts = []
for _ in range(50):
detections = detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
detection_counts.append(len(detections))
# 计算统计信息
result = {
'conf_threshold': conf_threshold,
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean(detection_counts)
}
conf_results.append(result)
logger.info(f"Conf threshold {conf_threshold}: {result['fps']:.2f} FPS, {result['avg_detections']:.1f} detections")
except Exception as e:
logger.error(f"Error testing confidence threshold {conf_threshold}: {e}")
results['confidence_threshold'] = conf_results
# 测试IOU阈值
iou_results = []
for iou_threshold in self.test_configs['iou_thresholds']:
logger.info(f"Testing IOU threshold: {iou_threshold}")
try:
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=0.5,
iou_threshold=iou_threshold,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
# 预热
for _ in range(5):
detector.detect(test_image)
# 测试推理性能
inference_times = []
detection_counts = []
for _ in range(50):
detections = detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
detection_counts.append(len(detections))
# 计算统计信息
result = {
'iou_threshold': iou_threshold,
'inference_mean': np.mean(inference_times),
'inference_std': np.std(inference_times),
'fps': 1000 / np.mean(inference_times),
'avg_detections': np.mean(detection_counts)
}
iou_results.append(result)
logger.info(f"IOU threshold {iou_threshold}: {result['fps']:.2f} FPS, {result['avg_detections']:.1f} detections")
except Exception as e:
logger.error(f"Error testing IOU threshold {iou_threshold}: {e}")
results['iou_threshold'] = iou_results
return results
def test_stability(self, duration: int = 300) -> Dict[str, Any]:
"""
长期稳定性测试
参数:
duration: 测试持续时间(秒)
返回:
测试结果字典
"""
logger.info(f"Running stability test for {duration} seconds...")
# 初始化组件
model_path = config_manager.get('model.path', 'models/yolov11n.pt')
input_size = config_manager.get('model.input_size', 640)
conf_threshold = config_manager.get('model.conf_threshold', 0.5)
iou_threshold = config_manager.get('model.iou_threshold', 0.45)
device = config_manager.get('model.device', 'auto')
detector = YOLODetector(
model_path=model_path,
input_size=input_size,
conf_threshold=conf_threshold,
iou_threshold=iou_threshold,
device=device
)
# 准备测试图像
test_image = self._generate_test_image(input_size, input_size)
# 启动系统监控
self._start_system_monitoring()
# 测试数据
inference_times = []
detection_counts = []
timestamps = []
# 预热
for _ in range(10):
detector.detect(test_image)
# 运行稳定性测试
start_time = time.time()
while time.time() - start_time < duration:
timestamp = time.time()
detections = detector.detect(test_image)
inference_times.append(detector.inference_time * 1000)
detection_counts.append(len(detections))
timestamps.append(timestamp)
# 控制测试频率
time.sleep(0.1)
# 停止系统监控
self._stop_system_monitoring()
# 计算统计信息
result = {
'duration': duration,
'total_frames': len(inference_times),
'inference': {
'mean': np.mean(inference_times),
'std': np.std(inference_times),
'min': np.min(inference_times),
'max': np.max(inference_times),
'median': np.median(inference_times),
'p95': np.percentile(inference_times, 95),
'p99': np.percentile(inference_times, 99)
},
'detections': {
'mean': np.mean(detection_counts),
'std': np.std(detection_counts),
'min': np.min(detection_counts),
'max': np.max(detection_counts)
},
'fps': len(inference_times) / duration,
'system_stats': self.system_stats
}
logger.info(f"Stability test completed: {result['fps']:.2f} FPS")
return result
def _generate_test_image(self, width: int, height: int) -> np.ndarray:
"""
生成测试图像
参数:
width: 图像宽度
height: 图像高度
返回:
测试图像
"""
# 生成随机图像
image = np.random.randint(0, 255, (height, width, 3), dtype=np.uint8)
# 添加一些简单的对象(矩形)
for _ in range(np.random.randint(1, 5)):
x1 = np.random.randint(0, width // 2)
y1 = np.random.randint(0, height // 2)
x2 = x1 + np.random.randint(20, width // 2)
y2 = y1 + np.random.randint(20, height // 2)
color = (np.random.randint(0, 255), np.random.randint(0, 255), np.random.randint(0, 255))
cv2.rectangle(image, (x1, y1), (x2, y2), color, -1)
return image
def _start_system_monitoring(self) -> None:
"""启动系统监控"""
self.monitoring = True
self.system_stats = []
self.monitor_thread = threading.Thread(target=self._monitor_system)
self.monitor_thread.daemon = True
self.monitor_thread.start()
def _stop_system_monitoring(self) -> None:
"""停止系统监控"""
self.monitoring = False
if self.monitor_thread:
self.monitor_thread.join(timeout=1)
def _monitor_system(self) -> None:
"""监控系统资源使用情况"""
while self.monitoring:
try:
# 获取CPU使用率
cpu_percent = psutil.cpu_percent(interval=0.1)
# 获取内存使用情况
memory = psutil.virtual_memory()
memory_percent = memory.percent
# 获取GPU使用情况(如果可用)
gpu_percent = 0
gpu_memory_percent = 0
if torch.cuda.is_available():
gpu_percent = torch.cuda.utilization()
gpu_memory_percent = torch.cuda.memory_allocated() / torch.cuda.max_memory_allocated() * 100
# 记录系统状态
stat = {
'timestamp': time.time(),
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'gpu_percent': gpu_percent,
'gpu_memory_percent': gpu_memory_percent
}
self.system_stats.append(stat)
except Exception as e:
logger.error(f"Error monitoring system: {e}")
def save_results(self) -> None:
"""保存测试结果"""
# 保存为JSON文件
results_file = os.path.join(self.output_dir, f"performance_test_{int(time.time())}.json")
with open(results_file, 'w') as f:
json.dump(self.results, f, indent=2)
logger.info(f"Test results saved to {results_file}")
# 生成报告
self._generate_report()
def _generate_report(self) -> None:
"""生成性能测试报告"""
report_file = os.path.join(self.output_dir, f"performance_report_{int(time.time())}.md")
with open(report_file, 'w') as f:
f.write("# YOLOv11 Performance Test Report\n\n")
f.write(f"Generated at: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
# 基础性能测试结果
if 'basic_performance' in self.results:
f.write("## Basic Performance Test\n\n")
basic = self.results['basic_performance']
f.write(f"- Model: {basic['model']}\n")
f.write(f"- Input Size: {basic['input_size']}\n")
f.write(f"- Device: {basic['device']}\n")
f.write(f"- FPS: {basic['fps']:.2f}\n")
f.write(f"- Average Detections: {basic['avg_detections']:.2f}\n")
f.write(f"- Inference Time (ms): {basic['inference']['mean']:.2f} ± {basic['inference']['std']:.2f}\n\n")
# 模型大小比较结果
if 'model_size_comparison' in self.results:
f.write("## Model Size Comparison\n\n")
f.write("| Model | FPS | Inference Time (ms) | Avg Detections |\n")
f.write("|-------|-----|---------------------|----------------|\n")
for result in self.results['model_size_comparison']:
f.write(f"| {result['model']} | {result['fps']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections']:.2f} |\n")
f.write("\n")
# 输入大小比较结果
if 'input_size_comparison' in self.results:
f.write("## Input Size Comparison\n\n")
f.write("| Input Size | FPS | Inference Time (ms) | Avg Detections |\n")
f.write("|------------|-----|---------------------|----------------|\n")
for result in self.results['input_size_comparison']:
f.write(f"| {result['input_size']} | {result['fps']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections']:.2f} |\n")
f.write("\n")
# 批处理大小比较结果
if 'batch_size_comparison' in self.results:
f.write("## Batch Size Comparison\n\n")
f.write("| Batch Size | FPS per Image | Inference Time (ms) | Avg Detections per Image |\n")
f.write("|------------|---------------|---------------------|---------------------------|\n")
for result in self.results['batch_size_comparison']:
f.write(f"| {result['batch_size']} | {result['fps_per_image']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections_per_image']:.2f} |\n")
f.write("\n")
# 设备比较结果
if 'device_comparison' in self.results:
f.write("## Device Comparison\n\n")
f.write("| Device | FPS | Inference Time (ms) | Avg Detections |\n")
f.write("|--------|-----|---------------------|----------------|\n")
for result in self.results['device_comparison']:
f.write(f"| {result['device']} | {result['fps']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections']:.2f} |\n")
f.write("\n")
# 参数敏感性测试结果
if 'parameter_sensitivity' in self.results:
f.write("## Parameter Sensitivity\n\n")
# 置信度阈值
f.write("### Confidence Threshold\n\n")
f.write("| Threshold | FPS | Inference Time (ms) | Avg Detections |\n")
f.write("|-----------|-----|---------------------|----------------|\n")
for result in self.results['parameter_sensitivity']['confidence_threshold']:
f.write(f"| {result['conf_threshold']} | {result['fps']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections']:.2f} |\n")
f.write("\n")
# IOU阈值
f.write("### IOU Threshold\n\n")
f.write("| Threshold | FPS | Inference Time (ms) | Avg Detections |\n")
f.write("|-----------|-----|---------------------|----------------|\n")
for result in self.results['parameter_sensitivity']['iou_threshold']:
f.write(f"| {result['iou_threshold']} | {result['fps']:.2f} | {result['inference_mean']:.2f} | {result['avg_detections']:.2f} |\n")
f.write("\n")
# 稳定性测试结果
if 'stability_test' in self.results:
f.write("## Stability Test\n\n")
stability = self.results['stability_test']
f.write(f"- Duration: {stability['duration']} seconds\n")
f.write(f"- Total Frames: {stability['total_frames']}\n")
f.write(f"- FPS: {stability['fps']:.2f}\n")
f.write(f"- Inference Time (ms): {stability['inference']['mean']:.2f} ± {stability['inference']['std']:.2f}\n")
f.write(f"- Min/Max Inference Time (ms): {stability['inference']['min']:.2f} / {stability['inference']['max']:.2f}\n")
f.write(f"- 95th Percentile Inference Time (ms): {stability['inference']['p95']:.2f}\n")
f.write(f"- 99th Percentile Inference Time (ms): {stability['inference']['p99']:.2f}\n")
f.write(f"- Average Detections: {stability['detections']['mean']:.2f}\n\n")
logger.info(f"Performance report generated at {report_file}")
def run_performance_tests() -> None:
"""运行性能测试"""
# 创建性能测试实例
test = PerformanceTest()
# 运行所有测试
results = test.run_all_tests()
# 打印摘要
print("\n=== Performance Test Summary ===")
if 'basic_performance' in results:
basic = results['basic_performance']
print(f"Basic Performance: {basic['fps']:.2f} FPS, {basic['inference']['mean']:.2f}ms inference time")
if 'model_size_comparison' in results:
print("\nModel Size Comparison:")
for result in results['model_size_comparison']:
print(f" {result['model']}: {result['fps']:.2f} FPS")
if 'device_comparison' in results:
print("\nDevice Comparison:")
for result in results['device_comparison']:
print(f" {result['device']}: {result['fps']:.2f} FPS")
print("\nTest results and report saved to 'test_results' directory")
if __name__ == "__main__":
run_performance_tests()