YOLOv8n 智能监控分析系统 v1.0
基于 YOLOv8n + PyQt5 的桌面端智能监控分析工具,支持视频分析、人物检索、车辆分析、监控录像回放,左右双画面实时对比展示。
作者信息
运行方式
pip install PyQt5 opencv-python ultralytics numpypython ysp/code/yolo_monitor.py
如果没有安装 ultralytics(YOLOv8),系统会自动切换到模拟检测模式,界面功能完整可用。
界面布局
+------------------------------------------------------------------+| YOLOv8n 智能监控分析系统 v1.0 |+------------------------------------------------------------------+| [视频源输入框] [选择文件] [摄像头] 置信度:[===] 速度:[1x] || [开始分析] [暂停] [停止] 轨迹 标签 分析中... |+---------------------------------+--------------------------------+| | || 原始画面 分析画面 | [统计] [告警] [检索] || +-----------+ +-----------+ | [设置] [关于] || | | | ID:1 人 | | || | 原始视频 | | +--+ | | 人数: 3 || | 无标注 | | | |框 | | 车辆: 2 || | | | +--+ | | 追踪ID: 5 || | | | ~~~轨迹 | | || +-----------+ +-----------+ | 历史趋势... || | |+---------------------------------+--------------------------------+| [10:23:45] 开始分析: test.mp4 || [10:23:46] 入侵告警: ID3 人 || 事件日志区域(最新在上) |+------------------------------------------------------------------+
核心特色:
- 左侧显示原始画面,右侧显示 AI 标注后的分析画面,实时对比
功能模块
视频源接入
- 本地视频文件(MP4/AVI/MOV/FLV/MKV)
- 倍速控制:0.5x / 1x / 2x / 4x / 8x
检测与追踪
告警功能
目标检索
统计报表
关键代码解析
关键代码一:基于 IoU 的多目标追踪器
这是整个系统的追踪核心,负责在连续帧之间维持目标的唯一 ID。
classSimpleTracker:def__init__(self, max_lost=30): self.next_id = 1 self.tracks = {} # id -> {"box", "cls", "lost", "trail"} self.max_lost = max_lostdefupdate(self, detections):# 1. 对每个已有轨迹,在新检测中找IoU最大的匹配for tid, track in self.tracks.items(): best_iou, best_idx = 0, -1for i, det_box in enumerate(det_boxes): iou = self._iou(track["box"], det_box)if iou > best_iou: best_iou, best_idx = iou, iif best_iou > 0.3:# 匹配成功:更新位置,重置丢失计数,记录轨迹点 track["box"] = det_boxes[best_idx] track["lost"] = 0 track["trail"].append(center_point)# 2. 未匹配的检测 -> 创建新轨迹,分配新ID# 3. 超过max_lost帧未匹配的轨迹 -> 删除 @staticmethoddef_iou(b1, b2):# 计算两个框的交并比 inter = 交集面积return inter / (面积1 + 面积2 - inter)
应用场景:
- 商场/园区人流量统计:每个人分配唯一 ID,进出计数不重复
- 交通路口车辆追踪:车辆穿越路口全程保持同一 ID,统计车流量
IoU 阈值 0.3 是经验值,适合中等速度移动的目标。快速移动场景可降低到 0.2,静态场景可提高到 0.5。max_lost=30 表示目标消失 30 帧后才删除轨迹,支持短暂遮挡后重新匹配。
关键代码二:视频处理线程与实时标注渲染
视频处理在独立 QThread 中运行,避免阻塞 GUI,通过信号将原始帧和标注帧同时传递给主窗口。
classVideoWorker(QThread): frame_ready = pyqtSignal(object, object, dict) # raw, annotated, statsdefrun(self): cap = cv2.VideoCapture(self.source)while self.running and cap.isOpened():if self.paused: continue ret, frame = cap.read() raw = frame.copy() # 保留原始帧 annotated, stats = self._process_frame(frame) # AI分析+标注 self.frame_ready.emit(raw, annotated, stats) # 双画面信号def_draw_annotations(self, frame, tracks, stats):for tid, t in tracks.items(): x1, y1, x2, y2 = t["box"] color = COLORS[t["cls"]]# 目标框 cv2.rectangle(frame, (x1,y1), (x2,y2), color, 2)# 标签(ID + 类型 + 置信度) label = f"ID:{tid}{name}{conf:.0%}" cv2.putText(frame, label, (x1, y1-4), ...)# 轨迹线(渐变粗细,越新越粗)for i in range(1, len(trail)): alpha = i / len(trail) thick = max(1, int(alpha * 3)) cv2.line(frame, trail[i-1], trail[i], color, thick)# 禁区入侵检测 cx, cy = 目标中心点for zone in self.alert_zones:if 中心点在禁区内: stats["alerts"].append("入侵告警")# 右上角统计信息叠加(半透明黑底) cv2.rectangle(overlay, ..., (0,0,0), -1) frame = cv2.addWeighted(overlay, 0.6, frame, 0.4, 0)
应用场景:
- 工厂安全监控:左侧原始画面供人工复核,右侧 AI 标注画面自动识别违规行为
- 交通监控中心:多路视频同时分析,每路都有原始+标注双画面
- 智慧社区:实时显示小区内人员/车辆分布,异常入侵即时告警
双画面设计的核心价值:原始画面保留完整证据链,标注画面提供 AI 辅助分析,两者对照可快速验证检测准确性。信号机制确保 GUI 不卡顿,即使 AI 推理耗时较长也不影响界面响应。
关键代码三:可交互的视频显示控件(禁区绘制)
自定义 QLabel 控件,支持鼠标右键拖拽绘制矩形禁区,实现"画面上直接标注"的交互体验。
classVideoLabel(QLabel): zone_drawn = pyqtSignal(int, int, int, int) # 绘制完成信号defmousePressEvent(self, e):if e.button() == Qt.RightButton: self._drawing = True self._start = e.pos() # 记录起点defmouseMoveEvent(self, e):if self._drawing: self._end = e.pos() # 实时更新终点 self.update() # 触发重绘,显示虚线框defmouseReleaseEvent(self, e):if e.button() == Qt.RightButton and self._drawing: self._drawing = False# 计算矩形区域,发射信号 x1 = min(start.x, end.x) y1 = min(start.y, end.y) x2 = max(start.x, end.x) y2 = max(start.y, end.y)if 面积足够大: self.zone_drawn.emit(x1, y1, x2, y2)defpaintEvent(self, e): super().paintEvent(e) # 先绘制视频帧if self._drawing:# 叠加绘制红色虚线矩形 + 半透明红色填充 painter.setPen(QPen(红色, 2, Qt.DashLine)) painter.setBrush(QBrush(半透明红色)) painter.drawRect(QRect(start, end))
应用场景:
- 安防部署:运维人员直接在监控画面上框选禁止进入区域,无需配置文件
- 工地安全:框选危险作业区,未佩戴安全帽人员进入即报警
这种"所见即所得"的交互方式大幅降低了使用门槛,非技术人员也能快速配置监控规则。信号-槽机制将绘制结果传递给 VideoWorker 的 alert_zones 列表,后续每帧检测时自动判断目标是否进入禁区。
代码架构
yolo_monitor.py+-- SimpleTracker # IoU多目标追踪器| +-- update() # 每帧更新追踪| +-- _iou() # 交并比计算+-- VideoWorker(QThread) # 视频处理线程| +-- run() # 主循环:读帧-检测-标注-发信号| +-- _process_frame() # YOLOv8推理| +-- _simulate_detect() # 无模型时的模拟检测| +-- _draw_annotations()# 标注渲染(框+标签+轨迹+禁区+统计)+-- VideoLabel(QLabel) # 可交互视频显示控件| +-- set_frame() # 显示OpenCV帧| +-- mouse*Event() # 右键拖拽绘制禁区| +-- paintEvent() # 叠加绘制虚线框+-- MonitorApp(QMainWindow)# 主窗口 +-- 顶部:视频源输入 + 参数控制 +-- 中间左:双画面(原始+标注) +-- 中间右:5个Tab(统计/告警/检索/设置/关于) +-- 底部:事件日志
数据流
视频源 (文件/RTSP/摄像头) | vVideoWorker.run() [子线程] | +-- cv2.VideoCapture 读帧 +-- YOLOv8n 推理 -> 检测框列表 +-- SimpleTracker.update() -> 带ID的追踪结果 +-- _draw_annotations() -> 标注帧 + 统计数据 | vframe_ready 信号 [跨线程] | +-- video_left.set_frame(原始帧) +-- video_right.set_frame(标注帧) +-- 更新统计数字 +-- 处理告警事件 -> 日志
检测类别
右侧功能 Tab
| |
|---|
| |
| |
| |
| 检测开关(人物/车辆)、入侵检测、聚集检测阈值、智能录像 |
| |
应用场景总结
依赖
PyQt5opencv-pythonultralytics # YOLOv8(可选,无则使用模拟模式)numpy
首次运行 ultralytics 会自动下载 yolov8n.pt 模型文件(约 6MB)。
-- coding: utf-8 --"""YOLOv8n 智能监控分析系统 v1.0功能:视频分析/人物检索/车辆分析/监控录像 - 左右双画面展示作者:杨少平 | 公众号:Python学在坚持运行:python yolo_monitor.py依赖:pip install PyQt5 opencv-python ultralytics numpy"""import sys, os, json, time, cv2, numpy as npfrom datetime import datetimefrom collections import defaultdict, dequefrom PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,QLabel, QLineEdit, QPushButton, QComboBox, QSlider, QCheckBox, QSpinBox,QTabWidget, QGroupBox, QScrollArea, QSplitter, QFileDialog, QMessageBox,QListWidget, QListWidgetItem, QFrame, QStatusBar, QTextEdit, QProgressBar,QTableWidget, QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QMenu)from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QPoint, QRectfrom PyQt5.QtGui import QFont, QColor, QImage, QPixmap, QPainter, QPen, QBrushAPP_VERSION = "v1.0"PERSON_CLASSES = [0] # COCO: personVEHICLE_CLASSES = [2, 3, 5, 7] # car, motorcycle, bus, truck==================== 简易追踪器 ====================class SimpleTracker:"""基于IoU的多目标追踪器"""def init(self, max_lost=30):self.next_id = 1self.tracks = {} # id -> {"box":, "cls":, "lost":, "trail": deque}self.max_lost = max_lostdef update(self, detections): """detections: list of (x1,y1,x2,y2,conf,cls)""" if not detections: for tid in list(self.tracks): self.tracks[tid]["lost"] += 1 if self.tracks[tid]["lost"] > self.max_lost: del self.tracks[tid] return {} det_boxes = [d[:4] for d in detections] det_cls = [int(d[5]) for d in detections] det_conf = [d[4] for d in detections] matched = {} used_det = set() # 匹配已有轨迹 for tid, track in list(self.tracks.items()): best_iou, best_idx = 0, -1 for i, db in enumerate(det_boxes): if i in used_det: continue iou = self._iou(track["box"], db) if iou > best_iou: best_iou, best_idx = iou, i if best_iou > 0.3 and best_idx >= 0: self.tracks[tid]["box"] = det_boxes[best_idx] self.tracks[tid]["cls"] = det_cls[best_idx] self.tracks[tid]["conf"] = det_conf[best_idx] self.tracks[tid]["lost"] = 0 cx = int((det_boxes[best_idx][0] + det_boxes[best_idx][2]) / 2) cy = int((det_boxes[best_idx][1] + det_boxes[best_idx][3]) / 2) self.tracks[tid]["trail"].append((cx, cy)) matched[tid] = self.tracks[tid] used_det.add(best_idx) else: self.tracks[tid]["lost"] += 1 if self.tracks[tid]["lost"] > self.max_lost: del self.tracks[tid] # 新目标 for i in range(len(det_boxes)): if i not in used_det: cx = int((det_boxes[i][0] + det_boxes[i][2]) / 2) cy = int((det_boxes[i][1] + det_boxes[i][3]) / 2) self.tracks[self.next_id] = { "box": det_boxes[i], "cls": det_cls[i], "conf": det_conf[i], "lost": 0, "trail": deque([(cx, cy)], maxlen=100) } matched[self.next_id] = self.tracks[self.next_id] self.next_id += 1 return matched@staticmethoddef _iou(b1, b2): x1 = max(b1[0], b2[0]); y1 = max(b1[1], b2[1]) x2 = min(b1[2], b2[2]); y2 = min(b1[3], b2[3]) inter = max(0, x2-x1) * max(0, y2-y1) a1 = (b1[2]-b1[0])*(b1[3]-b1[1]) a2 = (b2[2]-b2[0])*(b2[3]-b2[1]) return inter / (a1+a2-inter+1e-6)==================== 视频处理线程 ====================class VideoWorker(QThread):frame_ready = pyqtSignal(object, object, dict) # raw_frame, annotated_frame, statsfinished = pyqtSignal()def __init__(self): super().__init__() self.source = None self.running = False self.paused = False self.speed = 1.0 self.model = None self.tracker = SimpleTracker() self.conf_thresh = 0.4 self.show_trails = True self.show_labels = True self.alert_zones = [] # list of (x1,y1,x2,y2) 禁区 self.alert_lines = [] # list of (x1,y1,x2,y2) 警戒线 self.events = [] # 事件日志 self.person_count_history = deque(maxlen=300) self.vehicle_count_history = deque(maxlen=300)def set_source(self, source): self.source = source self.tracker = SimpleTracker()def load_model(self): try: from ultralytics import YOLO self.model = YOLO("yolov8n.pt") return True except Exception as e: print(f"模型加载失败: {e}") return Falsedef run(self): if not self.source: return cap = cv2.VideoCapture(self.source) if not cap.isOpened(): self.finished.emit(); return self.running = True while self.running and cap.isOpened(): if self.paused: time.sleep(0.05); continue ret, frame = cap.read() if not ret: break raw = frame.copy() annotated, stats = self._process_frame(frame) self.frame_ready.emit(raw, annotated, stats) # 控制速度 delay = max(1, int(30 / self.speed)) time.sleep(delay / 1000.0) cap.release() self.running = False self.finished.emit()def _process_frame(self, frame): stats = {"persons": 0, "vehicles": 0, "alerts": []} if self.model is None: # 无模型时用模拟检测 return self._simulate_detect(frame, stats) results = self.model(frame, conf=self.conf_thresh, verbose=False) detections = [] if results and len(results) > 0: boxes = results[0].boxes if boxes is not None: for box in boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() conf = float(box.conf[0]) cls = int(box.cls[0]) detections.append((x1, y1, x2, y2, conf, cls)) tracks = self.tracker.update(detections) return self._draw_annotations(frame, tracks, stats)def _simulate_detect(self, frame, stats): """无模型时的模拟检测(演示用)""" h, w = frame.shape[:2] import random fake_dets = [] for _ in range(random.randint(1, 4)): cx, cy = random.randint(w//4, 3*w//4), random.randint(h//4, 3*h//4) bw, bh = random.randint(30, 80), random.randint(60, 150) cls = random.choice([0, 0, 0, 2, 7]) # 偏向人 fake_dets.append((cx-bw, cy-bh, cx+bw, cy+bh, 0.85, cls)) tracks = self.tracker.update(fake_dets) return self._draw_annotations(frame, tracks, stats)def _draw_annotations(self, frame, tracks, stats): person_count = 0 vehicle_count = 0 NAMES = {0: "人", 2: "轿车", 3: "摩托", 5: "巴士", 7: "卡车"} COLORS = {0: (0,120,255), 2: (255,100,0), 3: (0,200,200), 5: (200,0,200), 7: (0,200,0)} # 绘制禁区 for z in self.alert_zones: cv2.rectangle(frame, (z[0],z[1]), (z[2],z[3]), (0,0,255), 2) cv2.putText(frame, "ALERT ZONE", (z[0],z[1]-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 1) # 绘制警戒线 for l in self.alert_lines: cv2.line(frame, (l[0],l[1]), (l[2],l[3]), (0,255,255), 2) for tid, t in tracks.items(): x1, y1, x2, y2 = [int(v) for v in t["box"]] cls = t["cls"] conf = t.get("conf", 0) color = COLORS.get(cls, (200, 200, 200)) name = NAMES.get(cls, f"cls{cls}") if cls in PERSON_CLASSES: person_count += 1 if cls in VEHICLE_CLASSES: vehicle_count += 1 # 目标框 cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) # 标签 if self.show_labels: label = f"ID:{tid}{name}{conf:.0%}" (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) cv2.rectangle(frame, (x1, y1-th-6), (x1+tw+4, y1), color, -1) cv2.putText(frame, label, (x1+2, y1-4), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 1) # 轨迹 if self.show_trails and len(t["trail"]) > 1: pts = list(t["trail"]) for i in range(1, len(pts)): alpha = i / len(pts) thick = max(1, int(alpha * 3)) cv2.line(frame, pts[i-1], pts[i], color, thick) # 禁区入侵检测 cx, cy = (x1+x2)//2, (y1+y2)//2 for z in self.alert_zones: if z[0] <= cx <= z[2] and z[1] <= cy <= z[3]: cv2.putText(frame, "!! INTRUSION !!", (x1, y1-20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,255), 2) stats["alerts"].append(f"入侵告警: ID{tid}{name}") # 统计信息叠加 stats["persons"] = person_count stats["vehicles"] = vehicle_count self.person_count_history.append(person_count) self.vehicle_count_history.append(vehicle_count) h, w = frame.shape[:2] overlay = frame.copy() cv2.rectangle(overlay, (w-220, 0), (w, 80), (0,0,0), -1) frame = cv2.addWeighted(overlay, 0.6, frame, 0.4, 0) cv2.putText(frame, f"Person: {person_count}", (w-210, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,200,255), 2) cv2.putText(frame, f"Vehicle: {vehicle_count}", (w-210, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,255,100), 2) cv2.putText(frame, datetime.now().strftime("%H:%M:%S"), (w-210, 73), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200,200,200), 1) return frame, statsdef stop(self): self.running = False==================== 视频显示控件 ====================class VideoLabel(QLabel):"""可显示视频帧的QLabel,支持鼠标绘制区域"""zone_drawn = pyqtSignal(int, int, int, int)def __init__(self, title=""): super().__init__() self.setAlignment(Qt.AlignCenter) self.setMinimumSize(320, 240) self.setStyleSheet("background:#1a1a2e;border:1px solid #333;border-radius:4px;") self.setText(title or "等待视频...") self.setFont(QFont("Microsoft YaHei", 12)) self._drawing = False self._start = QPoint() self._end = QPoint()def set_frame(self, frame): if frame is None: return h, w, ch = frame.shape bytes_per_line = ch * w img = QImage(frame.data, w, h, bytes_per_line, QImage.Format_BGR888) scaled = QPixmap.fromImage(img).scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.setPixmap(scaled)def mousePressEvent(self, e): if e.button() == Qt.RightButton: self._drawing = True self._start = e.pos()def mouseMoveEvent(self, e): if self._drawing: self._end = e.pos() self.update()def mouseReleaseEvent(self, e): if e.button() == Qt.RightButton and self._drawing: self._drawing = False self._end = e.pos() x1, y1 = min(self._start.x(), self._end.x()), min(self._start.y(), self._end.y()) x2, y2 = max(self._start.x(), self._end.x()), max(self._start.y(), self._end.y()) if abs(x2-x1) > 10 and abs(y2-y1) > 10: self.zone_drawn.emit(x1, y1, x2, y2)def paintEvent(self, e): super().paintEvent(e) if self._drawing: p = QPainter(self) p.setPen(QPen(QColor(255, 0, 0, 180), 2, Qt.DashLine)) p.setBrush(QBrush(QColor(255, 0, 0, 40))) p.drawRect(QRect(self._start, self._end)) p.end()==================== 主窗口 ====================class MonitorApp(QMainWindow):def init(self):super().init()self.setWindowTitle(f"🎯 YOLOv8n 智能监控分析系统 {APP_VERSION}")self.setMinimumSize(1200, 750)self.resize(1400, 850)self.worker = VideoWorker()self.worker.frame_ready.connect(self._on_frame)self.worker.finished.connect(self._on_finished)self.event_log = []self._build()def _build(self): central = QWidget() self.setCentralWidget(central) main_lay = QVBoxLayout(central) main_lay.setContentsMargins(6, 6, 6, 6) main_lay.setSpacing(6) # ---- 顶部控制栏 ---- top = QHBoxLayout() self.ed_source = QLineEdit() self.ed_source.setPlaceholderText("视频源: 文件路径 / RTSP地址 / 摄像头编号(0)") self.ed_source.setMinimumWidth(300) top.addWidget(self.ed_source) btn_file = QPushButton("📂 选择文件") btn_file.clicked.connect(self._select_file) top.addWidget(btn_file) btn_cam = QPushButton("📷 摄像头") btn_cam.clicked.connect(lambda: self.ed_source.setText("0")) top.addWidget(btn_cam) top.addWidget(QLabel("置信度:")) self.sl_conf = QSlider(Qt.Horizontal) self.sl_conf.setRange(10, 95) self.sl_conf.setValue(40) self.sl_conf.setMaximumWidth(100) self.lbl_conf = QLabel("0.40") self.sl_conf.valueChanged.connect(lambda v: (self.lbl_conf.setText(f"{v/100:.2f}"), setattr(self.worker, 'conf_thresh', v/100))) top.addWidget(self.sl_conf) top.addWidget(self.lbl_conf) top.addWidget(QLabel("速度:")) self.cb_speed = QComboBox() self.cb_speed.addItems(["0.5x", "1x", "2x", "4x", "8x"]) self.cb_speed.setCurrentIndex(1) self.cb_speed.currentTextChanged.connect(lambda t: setattr(self.worker, 'speed', float(t.replace('x','')))) top.addWidget(self.cb_speed) main_lay.addLayout(top) # ---- 播放控制 ---- ctrl = QHBoxLayout() self.btn_start = QPushButton("▶ 开始分析") self.btn_start.setStyleSheet("padding:8px 20px;background:#388e3c;color:#fff;border:none;border-radius:4px;font-size:13px;font-weight:bold;") self.btn_start.clicked.connect(self._start) self.btn_pause = QPushButton("⏸ 暂停") self.btn_pause.setStyleSheet("padding:8px 16px;background:#ff9800;color:#fff;border:none;border-radius:4px;") self.btn_pause.clicked.connect(self._pause) self.btn_stop = QPushButton("⏹ 停止") self.btn_stop.setStyleSheet("padding:8px 16px;background:#d32f2f;color:#fff;border:none;border-radius:4px;") self.btn_stop.clicked.connect(self._stop) self.chk_trails = QCheckBox("轨迹") self.chk_trails.setChecked(True) self.chk_trails.stateChanged.connect(lambda s: setattr(self.worker, 'show_trails', s == Qt.Checked)) self.chk_labels = QCheckBox("标签") self.chk_labels.setChecked(True) self.chk_labels.stateChanged.connect(lambda s: setattr(self.worker, 'show_labels', s == Qt.Checked)) ctrl.addWidget(self.btn_start) ctrl.addWidget(self.btn_pause) ctrl.addWidget(self.btn_stop) ctrl.addWidget(self.chk_trails) ctrl.addWidget(self.chk_labels) ctrl.addStretch() self.lbl_status = QLabel("⏹ 就绪") self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#888;") ctrl.addWidget(self.lbl_status) main_lay.addLayout(ctrl) # ---- 中间:左右双画面 + 右侧Tab ---- mid_splitter = QSplitter(Qt.Horizontal) # 左右视频画面 video_widget = QWidget() video_lay = QHBoxLayout(video_widget) video_lay.setContentsMargins(0, 0, 0, 0) video_lay.setSpacing(4) self.video_left = VideoLabel("📹 原始画面") self.video_left.setStyleSheet("background:#1a1a2e;color:#555;border:1px solid #333;border-radius:4px;") self.video_right = VideoLabel("🎯 分析画面") self.video_right.setStyleSheet("background:#1a1a2e;color:#555;border:1px solid #333;border-radius:4px;") self.video_right.zone_drawn.connect(self._add_zone) video_lay.addWidget(self.video_left) video_lay.addWidget(self.video_right) mid_splitter.addWidget(video_widget) # 右侧功能Tab right_tabs = QTabWidget() right_tabs.setMaximumWidth(360) right_tabs.setMinimumWidth(280) right_tabs.addTab(self._build_stats_tab(), "📊 统计") right_tabs.addTab(self._build_alert_tab(), "🚨 告警") right_tabs.addTab(self._build_search_tab(), "🔍 检索") right_tabs.addTab(self._build_settings_tab(), "⚙ 设置") right_tabs.addTab(self._build_about_tab(), "ℹ️ 关于") mid_splitter.addWidget(right_tabs) mid_splitter.setStretchFactor(0, 1) mid_splitter.setStretchFactor(1, 0) main_lay.addWidget(mid_splitter, 1) # ---- 底部事件日志 ---- self.log_list = QListWidget() self.log_list.setMaximumHeight(120) self.log_list.setStyleSheet("font-size:11px;background:#f8f8f8;") main_lay.addWidget(self.log_list) self.statusBar().showMessage(f"YOLOv8n 智能监控分析系统 {APP_VERSION} | 就绪")# ---- 右侧Tab构建 ----def _build_stats_tab(self): w = QWidget(); lay = QVBoxLayout(w); lay.setContentsMargins(8,8,8,8) lay.addWidget(QLabel("📊 实时统计")) self.lbl_person = QLabel("👤 人数: 0"); self.lbl_person.setStyleSheet("font-size:18px;font-weight:bold;color:#0078d4;"); lay.addWidget(self.lbl_person) self.lbl_vehicle = QLabel("🚗 车辆: 0"); self.lbl_vehicle.setStyleSheet("font-size:18px;font-weight:bold;color:#e65100;"); lay.addWidget(self.lbl_vehicle) self.lbl_tracks = QLabel("🔢 追踪ID: 0"); self.lbl_tracks.setStyleSheet("font-size:14px;color:#555;"); lay.addWidget(self.lbl_tracks) lay.addWidget(QFrame(frameShape=QFrame.HLine)) lay.addWidget(QLabel("📈 历史趋势")) self.stats_text = QTextEdit(); self.stats_text.setReadOnly(True); self.stats_text.setMaximumHeight(150) self.stats_text.setFont(QFont("Consolas", 9)); lay.addWidget(self.stats_text) lay.addWidget(QFrame(frameShape=QFrame.HLine)) btn_export = QPushButton("📥 导出统计报表") btn_export.setStyleSheet("padding:6px;background:#1976d2;color:#fff;border:none;border-radius:3px;") btn_export.clicked.connect(self._export_stats) lay.addWidget(btn_export) lay.addStretch() return wdef _build_alert_tab(self): w = QWidget(); lay = QVBoxLayout(w); lay.setContentsMargins(8,8,8,8) lay.addWidget(QLabel("🚨 告警设置")) lay.addWidget(QLabel("右键在分析画面上拖拽绘制禁区")) self.alert_list = QListWidget(); lay.addWidget(self.alert_list) btn_row = QHBoxLayout() btn_clear = QPushButton("清除所有禁区") btn_clear.clicked.connect(self._clear_zones) btn_row.addWidget(btn_clear) lay.addLayout(btn_row) lay.addWidget(QFrame(frameShape=QFrame.HLine)) lay.addWidget(QLabel("告警日志:")) self.alert_log = QListWidget(); self.alert_log.setStyleSheet("font-size:11px;"); lay.addWidget(self.alert_log, 1) return wdef _build_search_tab(self): w = QWidget(); lay = QVBoxLayout(w); lay.setContentsMargins(8,8,8,8) lay.addWidget(QLabel("🔍 目标检索")) g = QFormLayout() self.search_type = QComboBox(); self.search_type.addItems(["人物", "车辆", "全部"]) g.addRow("目标类型:", self.search_type) self.search_id = QLineEdit(); self.search_id.setPlaceholderText("输入追踪ID") g.addRow("追踪ID:", self.search_id) lay.addLayout(g) btn = QPushButton("🔍 检索") btn.setStyleSheet("padding:6px;background:#d32f2f;color:#fff;border:none;border-radius:3px;") btn.clicked.connect(self._do_search) lay.addWidget(btn) self.search_result = QTextEdit(); self.search_result.setReadOnly(True) self.search_result.setFont(QFont("Consolas", 9)); lay.addWidget(self.search_result, 1) return wdef _build_settings_tab(self): w = QWidget(); lay = QVBoxLayout(w); lay.setContentsMargins(8,8,8,8) lay.addWidget(QLabel("⚙ 检测设置")) g = QFormLayout() self.chk_person = QCheckBox("检测人物"); self.chk_person.setChecked(True); g.addRow(self.chk_person) self.chk_vehicle = QCheckBox("检测车辆"); self.chk_vehicle.setChecked(True); g.addRow(self.chk_vehicle) self.chk_intrusion = QCheckBox("入侵检测"); self.chk_intrusion.setChecked(True); g.addRow(self.chk_intrusion) self.chk_crowd = QCheckBox("人群聚集检测"); self.chk_crowd.setChecked(True); g.addRow(self.chk_crowd) self.sp_crowd = QSpinBox(); self.sp_crowd.setRange(2, 50); self.sp_crowd.setValue(5) g.addRow("聚集阈值:", self.sp_crowd) self.chk_record = QCheckBox("智能录像(检测到目标时录制)"); g.addRow(self.chk_record) lay.addLayout(g) lay.addStretch() return wdef _build_about_tab(self): w = QWidget(); lay = QVBoxLayout(w); lay.setContentsMargins(12,20,12,12); lay.setAlignment(Qt.AlignTop) t = QLabel(f"🎯 YOLOv8n 智能监控\n{APP_VERSION}"); t.setStyleSheet("font-size:16px;font-weight:bold;color:#d32f2f;"); lay.addWidget(t) lay.addSpacing(10) for k, v in [("作者", "杨少平"), ("公众号", "Python学在坚持"), ("微信", "ysp2338084")]: r = QHBoxLayout() r.addWidget(QLabel(k+":")); l = QLabel(v); l.setStyleSheet("font-weight:bold;"); l.setTextInteractionFlags(Qt.TextSelectableByMouse); r.addWidget(l); r.addStretch() lay.addLayout(r) lay.addSpacing(10) desc = QLabel("功能: YOLOv8n实时检测 | 人物/车辆追踪\n区域入侵告警 | 轨迹可视化 | 统计报表\n支持: 本地视频/RTSP流/USB摄像头") desc.setStyleSheet("color:#666;font-size:11px;"); desc.setWordWrap(True); lay.addWidget(desc) lay.addStretch() return w# ---- 核心操作 ----def _select_file(self): path, _ = QFileDialog.getOpenFileName(self, "选择视频", "", "视频文件 (*.mp4 *.avi *.mov *.flv *.mkv);;所有文件 (*)") if path: self.ed_source.setText(path)def _start(self): src = self.ed_source.text().strip() if not src: QMessageBox.warning(self, "提示", "请输入视频源"); return # 尝试转为摄像头编号 try: src = int(src) except ValueError: pass self.worker.set_source(src) if self.worker.model is None: self._log("正在加载YOLOv8n模型...") if not self.worker.load_model(): self._log("⚠ 模型加载失败,使用模拟检测模式") self.worker.start() self.lbl_status.setText("▶ 分析中...") self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#388e3c;") self._log(f"开始分析: {src}")def _pause(self): self.worker.paused = not self.worker.paused if self.worker.paused: self.btn_pause.setText("▶ 继续") self.lbl_status.setText("⏸ 已暂停"); self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#ff9800;") else: self.btn_pause.setText("⏸ 暂停") self.lbl_status.setText("▶ 分析中..."); self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#388e3c;")def _stop(self): self.worker.stop() self.lbl_status.setText("⏹ 已停止"); self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#d32f2f;") self._log("分析已停止")def _on_frame(self, raw, annotated, stats): self.video_left.set_frame(raw) self.video_right.set_frame(annotated) self.lbl_person.setText(f"👤 人数: {stats['persons']}") self.lbl_vehicle.setText(f"🚗 车辆: {stats['vehicles']}") self.lbl_tracks.setText(f"🔢 追踪ID: {len(self.worker.tracker.tracks)}") for alert in stats.get("alerts", []): ts = datetime.now().strftime("%H:%M:%S") self.alert_log.insertItem(0, f"[{ts}] {alert}") self._log(f"🚨 {alert}")def _on_finished(self): self.lbl_status.setText("⏹ 完成"); self.lbl_status.setStyleSheet("font-size:13px;font-weight:bold;color:#888;") self._log("视频分析完成")def _add_zone(self, x1, y1, x2, y2): self.worker.alert_zones.append((x1, y1, x2, y2)) self.alert_list.addItem(f"禁区 #{len(self.worker.alert_zones)}: ({x1},{y1})-({x2},{y2})") self._log(f"添加禁区: ({x1},{y1})-({x2},{y2})")def _clear_zones(self): self.worker.alert_zones.clear() self.alert_list.clear() self._log("已清除所有禁区")def _do_search(self): tid = self.search_id.text().strip() if not tid: self.search_result.setPlainText("请输入追踪ID"); return try: tid = int(tid) except ValueError: self.search_result.setPlainText("ID必须是数字"); return track = self.worker.tracker.tracks.get(tid) if track: NAMES = {0:"人",2:"轿车",3:"摩托",5:"巴士",7:"卡车"} lost = track["lost"] status = "活跃" if lost == 0 else f"丢失{lost}帧" self.search_result.setPlainText( f"ID: {tid}\n类型: {NAMES.get(track['cls'], '未知')}\n" f"位置: {[int(v) for v in track['box']]}\n" f"轨迹点数: {len(track['trail'])}\n" f"状态: {status}" ) else: self.search_result.setPlainText(f"未找到ID={tid}的目标")def _export_stats(self): path, _ = QFileDialog.getSaveFileName(self, "导出统计", "stats.txt", "文本文件 (*.txt)") if path: with open(path, "w", encoding="utf-8") as f: f.write(f"YOLOv8n 监控统计报表\n生成时间: {datetime.now()}\n\n") f.write(f"当前追踪目标数: {len(self.worker.tracker.tracks)}\n") f.write(f"人数历史: {list(self.worker.person_count_history)[-20:]}\n") f.write(f"车辆历史: {list(self.worker.vehicle_count_history)[-20:]}\n\n") f.write("事件日志:\n") for e in self.event_log[-100:]: f.write(f" {e}\n") QMessageBox.information(self, "成功", f"已导出到 {path}")def _log(self, msg): ts = datetime.now().strftime("%H:%M:%S") entry = f"[{ts}] {msg}" self.event_log.append(entry) self.log_list.insertItem(0, entry) if self.log_list.count() > 500: self.log_list.takeItem(self.log_list.count() - 1)def closeEvent(self, e): self.worker.stop() self.worker.wait(2000) e.accept()==================== 启动 ====================if name == "main":app = QApplication(sys.argv)app.setStyle("Fusion")app.setFont(QFont("Microsoft YaHei", 10))win = MonitorApp()win.show()sys.exit(app.exec_())