更新多个算法问题

main
石超 2026-02-05 10:39:03 +08:00
parent 91e4522e2c
commit 70f2bc2bf3
60 changed files with 22271 additions and 74972 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ yolo_detection.log
*.pyc
encrypted_models
temp_uploads
*.pt
video

Binary file not shown.

Binary file not shown.

View File

@ -150,11 +150,6 @@ class ModelManager:
device = model_config.get('device', 'cuda:0' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
# 应用半精度配置
if model_config.get('half', False) and 'cuda' in device:
model = model.half()
logger.info(f"启用半精度推理: {model_path}")
# 缓存模型
with self.cache_lock:
self.model_cache[cache_key] = {
@ -1182,7 +1177,9 @@ class DetectionThread(threading.Thread):
# 多模型推理
start = time.time()
annotated_frame, model_detections = self._multi_model_inference(frame)
logger.info(f'startTime:{start},endTime:{time.time()},时间差:{time.time() - start}')
# annotated_frame = frame
# model_detections = []
# logger.info(f'startTime:{start},endTime:{time.time()},时间差:{time.time() - start}')
# 推流处理Windows优化
if self.enable_push:
if not self.push_frame_to_task_streamer(annotated_frame):
@ -1191,18 +1188,19 @@ class DetectionThread(threading.Thread):
if current_time - self.last_log_time >= 1:
# # WebSocket发送
self.send_to_websocket(model_detections)
# self.send_to_websocket(model_detections)
# # 上传处理
# self.handle_upload(annotated_frame, model_detections, current_time)
# 检查推流健康状态
if self.enable_push:
if not self.check_push_health():
# 如果推流不健康,且当前不是错误状态,则更新为降级状态
current_status = getattr(self, '_current_status', 'running')
if current_status != 'error' and current_status != 'degraded':
self.update_task_status('degraded')
# self.update_task_status('degraded')
pass
self.last_log_time = current_time
self.frame_count += 1

View File

@ -1,544 +1,333 @@
import cv2
import torch
import numpy as np
import concurrent.futures
from typing import List, Dict, Tuple, Any, Optional, Union
from dataclasses import dataclass
from collections import defaultdict
from PIL import Image, ImageDraw, ImageFont
# 尝试使用PIL绘制中文
font = None
FONT_PATHS = [
"simhei.ttf", # 黑体
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", # 文泉驿微米黑
"C:/Windows/Fonts/simhei.ttf", # Windows黑体
"/System/Library/Fonts/PingFang.ttc" # macOS苹方
]
for path in FONT_PATHS:
try:
font = ImageFont.truetype(path, 20)
break
except:
continue
@dataclass
class ModelConfig:
"""模型配置数据类"""
model: Any
config: Any # 应该是模型的配置参数
tags: Optional[Dict[str, Dict]] = None
name: str = "未命名"
id: Any = None
device: str = "cuda:0" # 模型运行的设备 cuda0表示第一个GPU
conf_thres: float = 0.25 # 置信度阈值
iou_thres: float = 0.45 # iou 衡量两个边界框的重叠程度,过低会误检,过高会漏检
half: bool = False # 是否使用半精度推理
key_valid: Any = None # 密钥验证结果
model_hash: Any = None # 模型哈希值
imgsz: Any = 640 # 输入图像尺寸
def __post_init__(self):
"""初始化后处理"""
if self.tags is None:
self.tags = {}
@dataclass
class Detection:
"""检测结果数据类"""
model_idx: int # 模型索引
model_name: str # 模型名称
boxes: np.ndarray # 边界框坐标
confidences: np.ndarray # 置信度
class_ids: np.ndarray # 类别索引
class_names: List[str] # 类别名称
"""
# 存储每个类别的标签配置信息
# 包含颜色、置信度阈值、是否绘制等信息
# 用于控制检测结果的可视化
"""
tags: Dict[str, Dict] # 标签配置 如何绘制检测结果
raw_result: Any = None # 新增:保存原始结果对象
class DetectionVisualizer:
"""检测结果可视化器"""
# 预定义模型颜色
MODEL_COLORS = [
(0, 255, 0), # 绿色
(255, 0, 0), # 蓝色
(0, 0, 255), # 红色
(255, 255, 0), # 青色
(255, 0, 255), # 紫色
(0, 255, 255), # 黄色
(128, 0, 128), # 深紫色
(0, 128, 128), # 橄榄色
]
def __init__(self, use_pil: bool = True):
"""
初始化可视化器
Args:
use_pil: 是否使用PIL绘制支持中文
"""
self.use_pil = use_pil
self.font = font
def should_draw_detection(self, class_id: int, confidence: float,
tags: Dict[str, Dict]) -> Tuple[bool, Optional[Tuple[int, int, int]]]:
"""
判断是否应该绘制检测框
Args:
class_id: 类别ID
confidence: 置信度
tags: 标签配置字典
Returns:
(是否绘制, 颜色)
"""
class_id_str = str(class_id)
# 如果标签配置为空,默认不绘制任何标签(因为需求是只绘制配置的标签)
if not tags:
return False, None
# 如果标签不在配置中,不绘制
tag_config = tags.get(class_id_str)
if not tag_config:
return False, None
# 检查select标记
if not tag_config.get('select', True):
return False, None
# 检查置信度阈值
reliability = tag_config.get('reliability', 0)
if confidence < reliability:
return False, None
# 获取自定义颜色
color = tag_config.get('color')
if color and isinstance(color, (list, tuple)) and len(color) >= 3:
return True, tuple(color[:3])
return True, None
def draw_with_pil(self, frame: np.ndarray, detections: List[Detection],
confidence_threshold: float) -> np.ndarray:
"""使用PIL绘制检测结果支持中文"""
# 转换到PIL格式
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)
draw = ImageDraw.Draw(pil_image)
for det in detections:
model_idx = det.model_idx
boxes = det.boxes
confidences = det.confidences
class_names = det.class_names
class_ids = det.class_ids
tags = det.tags
for i, (box, conf, cls_id, cls_name) in enumerate(
zip(boxes, confidences, class_ids, class_names)):
if conf < confidence_threshold:
continue
should_draw, custom_color = self.should_draw_detection(
cls_id, conf, tags)
if not should_draw:
continue
# 使用自定义颜色或模型颜色
color = custom_color or self.MODEL_COLORS[model_idx % len(self.MODEL_COLORS)]
x1, y1, x2, y2 = map(int, box[:4])
# 标签文本
label = f"{cls_name} {conf:.2f}"
# 绘制矩形框
draw.rectangle([x1, y1, x2, y2], outline=color, width=2)
# 绘制标签背景
if self.font:
try:
text_bbox = draw.textbbox((x1, y1 - 25), label, font=self.font)
draw.rectangle(text_bbox, fill=color)
draw.text((x1, y1 - 25), label, fill=(255, 255, 255), font=self.font)
except:
# 字体失败回退到OpenCV
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
else:
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
# 转换回OpenCV格式
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
def draw_with_opencv(self, frame: np.ndarray, detections: List[Detection],
confidence_threshold: float) -> np.ndarray:
"""使用OpenCV绘制检测结果"""
frame_drawn = frame.copy()
for det in detections:
model_idx = det.model_idx
model_name = det.model_name
boxes = det.boxes
confidences = det.confidences
class_names = det.class_names
class_ids = det.class_ids
tags = det.tags
for i, (box, conf, cls_id, cls_name) in enumerate(
zip(boxes, confidences, class_ids, class_names)):
if conf < confidence_threshold:
continue
should_draw, custom_color = self.should_draw_detection(
cls_id, conf, tags)
if not should_draw:
continue
# 使用自定义颜色或模型颜色
color = custom_color or self.MODEL_COLORS[model_idx % len(self.MODEL_COLORS)]
x1, y1, x2, y2 = map(int, box[:4])
# 绘制矩形框
cv2.rectangle(frame_drawn, (x1, y1), (x2, y2), color, 2)
# 绘制标签
label = f"{model_name}: {cls_name} {conf:.2f}"
# 计算文本大小
(text_width, text_height), baseline = cv2.getTextSize(
label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2
)
# 绘制标签背景
cv2.rectangle(frame_drawn,
(x1, y1 - text_height - 10),
(x1 + text_width, y1),
color, -1)
# 绘制文本
cv2.putText(frame_drawn, label, (x1, y1 - 5),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
return frame_drawn
def draw(self, frame: np.ndarray, detections: List[Detection],
confidence_threshold: float) -> np.ndarray:
"""绘制检测结果"""
if self.use_pil:
try:
return self.draw_with_pil(frame, detections, confidence_threshold)
except Exception as e:
print(f"PIL绘制失败使用OpenCV: {e}")
return self.draw_with_opencv(frame, detections, confidence_threshold)
else:
return self.draw_with_opencv(frame, detections, confidence_threshold)
class YOLOModelWrapper:
"""YOLO模型包装器"""
@staticmethod
def infer_yolov8(model: Any, frame: np.ndarray, conf_thres: float,
iou_thres: float, imgsz: int, half: bool, device: str):
"""YOLOv8推理"""
with torch.no_grad():
results = model(frame, conf=conf_thres, verbose=False,
iou=iou_thres, imgsz=imgsz, half=half, device=device)
if not results or len(results) == 0:
return np.array([]), np.array([]), np.array([]), [], None
result = results[0]
if not hasattr(result, 'boxes'):
return np.array([]), np.array([]), np.array([]), [], None
boxes = result.boxes.xyxy.cpu().numpy()
confidences = result.boxes.conf.cpu().numpy()
class_ids = result.boxes.cls.cpu().numpy().astype(int)
class_names = [result.names.get(cid, str(cid)) for cid in class_ids]
return boxes, confidences, class_ids, class_names, result
@staticmethod
def infer_yolov5(model: Any, frame: np.ndarray):
"""YOLOv5推理"""
results = model(frame)
if not hasattr(results, 'xyxy'):
return np.array([]), np.array([]), np.array([]), [], None
detections = results.xyxy[0].cpu().numpy()
if len(detections) == 0:
return np.array([]), np.array([]), np.array([]), [], None
boxes = detections[:, :4]
confidences = detections[:, 4]
class_ids = detections[:, 5].astype(int)
if hasattr(results, 'names'):
class_names = [results.names.get(cid, str(cid)) for cid in class_ids]
else:
class_names = [str(cid) for cid in class_ids]
return boxes, confidences, class_ids, class_names, results
from typing import List, Tuple
import cv2
from concurrent.futures import ThreadPoolExecutor, as_completed
def multi_model_inference(
models_config: List[Union[Dict, ModelConfig]],
models: List[dict],
frame: np.ndarray,
confidence_threshold: float = 0.25,
parallel: bool = True,
use_pil: bool = True,
use_plot_for_single: bool = True # 新增参数是否对单个模型使用plot绘制
) -> Tuple[np.ndarray, List[Detection]]:
use_plot_for_single: bool = True
) -> Tuple[np.ndarray, List[dict]]:
"""
多模型并行推理
Args:
models_config: 模型配置列表
models: 已加载的模型列表每个元素是包含'model'键的字典
frame: 视频帧 (BGR格式)
confidence_threshold: 全局置信度阈值
parallel: 是否并行推理
use_pil: 是否使用PIL绘制
use_plot_for_single: 当只有一个模型时是否使用result.plot()绘制
Returns:
(绘制完成的帧, 检测结果列表)
"""
# 转换为ModelConfig对象
configs = []
for cfg in models_config:
if isinstance(cfg, dict):
configs.append(ModelConfig(**cfg))
else:
configs.append(cfg)
if len(models) == 0:
return frame, []
def single_model_inference(model_cfg: ModelConfig, model_idx: int) -> Detection:
"""单个模型的推理函数"""
original_frame = frame.copy()
def inference_single_model(model_info: dict, img: np.ndarray) -> dict:
"""单模型推理"""
try:
model = model_cfg.model
model = model_info['model']
# 使用模型配置中的参数,如果不存在则使用默认值
conf_thres = model_info.get('conf_thres', confidence_threshold)
iou_thres = model_info.get('iou_thres', 0.45)
imgsz = model_info.get('imgsz', 640)
device = model_info.get('device', 'cpu')
# 根据模型类型进行推理
if hasattr(model, 'predict'): # YOLOv8
boxes, confidences, class_ids, class_names, raw_result = YOLOModelWrapper.infer_yolov8(
model, frame, model_cfg.conf_thres, model_cfg.iou_thres,
model_cfg.imgsz, model_cfg.half, model_cfg.device
)
elif hasattr(model, '__call__'): # YOLOv5
boxes, confidences, class_ids, class_names, raw_result = YOLOModelWrapper.infer_yolov5(model, frame)
else:
raise ValueError(f"不支持的模型类型: {type(model)}")
return Detection(
model_idx=model_idx,
model_name=model_cfg.name,
boxes=boxes,
confidences=confidences,
class_ids=class_ids,
class_names=class_names,
tags=model_cfg.tags,
raw_result=raw_result
# 执行推理
results = model.predict(
source=img,
imgsz=imgsz,
conf=conf_thres,
iou=iou_thres,
verbose=False,
device=device,
max_det=300
)
result = results[0] if len(results) > 0 else None
model_name = model_info.get('name', f"model_{model_info.get('id', 'unknown')}")
model_id = model_info.get('id', 0)
if result and result.boxes is not None and len(result.boxes):
# 提取检测信息
boxes_xyxy = result.boxes.xyxy.cpu().numpy()
confidences = result.boxes.conf.cpu().numpy()
class_ids = result.boxes.cls.cpu().numpy().astype(int)
class_names = [result.names[int(cls_id)] for cls_id in class_ids]
return {
'model_idx': model_id,
'model_name': model_name,
'boxes': boxes_xyxy,
'confidences': confidences,
'class_ids': class_ids,
'class_names': class_names,
'tags': model_info.get('tags', []),
'raw_result': result,
'success': True
}
else:
return {
'model_idx': model_id,
'model_name': model_name,
'boxes': np.array([]),
'confidences': np.array([]),
'class_ids': np.array([]),
'class_names': [],
'tags': model_info.get('tags', []),
'raw_result': result,
'success': False
}
except Exception as e:
print(f"模型 {model_idx} ({model_cfg.name}) 推理失败: {e}")
return Detection(
model_idx=model_idx,
model_name=model_cfg.name,
boxes=np.array([]),
confidences=np.array([]),
class_ids=np.array([]),
class_names=[],
tags=model_cfg.tags,
raw_result=None
)
# 并行推理
if parallel and len(configs) > 1:
detections = []
with concurrent.futures.ThreadPoolExecutor(max_workers=len(configs)) as executor:
# 提交任务
future_to_idx = {
executor.submit(single_model_inference, cfg, idx): idx
for idx, cfg in enumerate(configs)
print(f"模型 {model_info.get('name', 'unknown')} 推理失败: {str(e)}")
model_id = model_info.get('id', 0)
model_name = model_info.get('name', f"model_{model_id}")
return {
'model_idx': model_id,
'model_name': model_name,
'boxes': np.array([]),
'confidences': np.array([]),
'class_ids': np.array([]),
'class_names': [],
'tags': model_info.get('tags', []),
'raw_result': None,
'success': False
}
# 执行推理
detections = []
if parallel and len(models) > 1:
# 并行推理
max_workers = min(len(models), 4)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交推理任务
future_to_model = {}
for model_info in models:
future = executor.submit(inference_single_model, model_info, original_frame)
future_to_model[future] = model_info
# 收集结果
for future in concurrent.futures.as_completed(future_to_idx):
for future in as_completed(future_to_model.keys()):
try:
detection = future.result(timeout=2.0)
detections.append(detection)
except concurrent.futures.TimeoutError:
idx = future_to_idx[future]
print(f"模型 {idx} 推理超时")
# 创建空的检测结果
detections.append(Detection(
model_idx=idx,
model_name=configs[idx].name,
boxes=np.array([]),
confidences=np.array([]),
class_ids=np.array([]),
class_names=[],
tags=configs[idx].tags,
raw_result=None
))
result = future.result(timeout=30.0)
detections.append(result)
except Exception as e:
print(f"模型推理异常: {e}")
model_info = future_to_model[future]
model_name = model_info.get('name', 'unknown')
print(f"并行推理任务失败 - 模型 {model_name}: {str(e)}")
else:
# 顺序推理
detections = [single_model_inference(cfg, idx)
for idx, cfg in enumerate(configs)]
# 串行推理
for model_info in models:
result = inference_single_model(model_info, original_frame)
detections.append(result)
# 按照模型索引排序
detections.sort(key=lambda x: x.model_idx)
# 结果绘制
plotted_frame = original_frame.copy()
# 新增如果只有一个模型且配置使用plot绘制则使用result.plot()
if len(configs) == 1 and use_plot_for_single:
single_detection = detections[0]
if single_detection.raw_result is not None:
if len(detections) == 1 and use_plot_for_single:
# 单个模型直接使用YOLO的plot()方法
detection = detections[0]
if detection['success'] and detection['raw_result'] is not None:
try:
# 检查是否有plot方法
if hasattr(single_detection.raw_result, 'plot'):
# 使用plot方法绘制结果
frame_drawn = single_detection.raw_result.plot()
# 确保返回的是numpy数组
if not isinstance(frame_drawn, np.ndarray):
frame_drawn = np.array(frame_drawn)
# plot方法通常返回RGB图像转换为BGR
if len(frame_drawn.shape) == 3 and frame_drawn.shape[2] == 3:
frame_drawn = cv2.cvtColor(frame_drawn, cv2.COLOR_RGB2BGR)
# print(f"使用 {single_detection.model_name} 的 plot() 方法绘制结果")
return frame_drawn, detections
# 使用YOLO自带的plot方法
plotted_frame_rgb = detection['raw_result'].plot(
img=original_frame,
conf=True,
labels=True,
boxes=True,
line_width=2
)
# 确保返回的是BGR格式
if plotted_frame_rgb.shape[-1] == 3:
plotted_frame = cv2.cvtColor(plotted_frame_rgb, cv2.COLOR_RGB2BGR)
else:
print(f"模型 {single_detection.model_name} 的结果对象没有 plot() 方法,使用自定义绘制")
plotted_frame = plotted_frame_rgb
except Exception as e:
print(f"使用 plot() 方法绘制失败: {e},回退到自定义绘制")
print(f"使用YOLO plot()绘制失败: {str(e)}")
# 单个模型时不使用自定义绘制,直接返回原图
plotted_frame = original_frame.copy()
else:
plotted_frame = original_frame.copy()
# 绘制结果
visualizer = DetectionVisualizer(use_pil=use_pil)
frame_drawn = visualizer.draw(frame.copy(), detections, confidence_threshold)
elif len(detections) > 0:
# 多个模型使用自定义绘制
plotted_frame = plot_custom_results(original_frame, detections)
else:
plotted_frame = original_frame.copy()
return frame_drawn, detections
return plotted_frame, detections
# 兼容旧接口
def multi_model_inference_legacy(_models: List[Dict], frame: np.ndarray,
confidence_threshold: float = 0.25,
parallel: bool = True,
use_plot_for_single: bool = True) -> Tuple[np.ndarray, List[Dict]]:
"""旧接口兼容函数"""
frame_drawn, detections = multi_model_inference(
_models, frame, confidence_threshold, parallel, use_plot_for_single=use_plot_for_single
)
def plot_custom_results(frame: np.ndarray, detections: List[dict]) -> np.ndarray:
"""自定义绘制多个模型的结果"""
result_frame = frame.copy()
# 转换为旧格式
old_detections = []
for det in detections:
old_detections.append({
'model_idx': det.model_idx,
'model_name': det.model_name,
'boxes': det.boxes,
'confidences': det.confidences,
'class_ids': det.class_ids,
'class_names': det.class_names,
'tags': det.tags
})
return frame_drawn, old_detections
if __name__ == '__main__':
from ultralytics import YOLO
print('加载模型中')
model_paths = [
r"F:\PyModelScope\Yolov\models\yolov8m.pt",
r"F:\PyModelScope\Yolov\models\car.pt"
# 预定义的颜色列表
colors = [
(0, 255, 0), # 绿色
(255, 0, 0), # 蓝色
(0, 0, 255), # 红色
(255, 255, 0), # 青色
(255, 0, 255), # 洋红
(0, 255, 255), # 黄色
(128, 0, 128), # 紫色
(0, 128, 128), # 橄榄色
]
print("预热模型...")
model_list = []
for i, path in enumerate(model_paths):
model = YOLO(path)
model.to('cuda:0' if torch.cuda.is_available() else 'cpu')
model.eval()
for idx, detection in enumerate(detections):
if not detection['success'] or len(detection['boxes']) == 0:
continue
# 创建模型配置
model_config = ModelConfig(
model=model,
name=f'model_{i}',
device='cuda:0',
conf_thres=0.45,
iou_thres=0.45,
half=False,
imgsz=1920,
tags={
"0": {"name": "汽车", "reliability": 0.4, "select": True, "color": [0, 255, 0]},
"1": {"name": "行人", "reliability": 0.3, "select": True, "color": [255, 0, 0]},
"2": {"name": "自行车", "reliability": 0.5, "select": False, "color": [0, 0, 255]}
},
config=None
)
model_list.append(model_config)
color = colors[idx % len(colors)]
model_name = detection['model_name']
print("模型预热完成")
image_path = r"F:\PyModelScope\Yolov\images\444.png"
frame = cv2.imread(image_path)
# 绘制每个检测框
for box, conf, cls_id, cls_name in zip(
detection['boxes'],
detection['confidences'],
detection['class_ids'],
detection['class_names']
):
x1, y1, x2, y2 = map(int, box)
# 测试单个模型的情况 - 使用plot绘制
print("\n=== 测试单个模型 (使用plot绘制) ===")
single_model_list = [model_list[0]] # 只使用第一个模型
frame_drawn_single, detections_single = multi_model_inference(
single_model_list, frame, use_plot_for_single=True
)
print(f"单个模型检测结果: {len(detections_single[0].boxes)} 个目标")
cv2.imwrite("uploads/result_single_plot.jpg", frame_drawn_single)
print("结果已保存到 uploads/result_single_plot.jpg")
# 绘制边界框
cv2.rectangle(result_frame, (x1, y1), (x2, y2), color, 2)
# 测试单个模型的情况 - 强制使用自定义绘制
print("\n=== 测试单个模型 (强制使用自定义绘制) ===")
frame_drawn_single_custom, detections_single_custom = multi_model_inference(
single_model_list, frame, use_plot_for_single=False
)
print(f"单个模型自定义绘制结果: {len(detections_single_custom[0].boxes)} 个目标")
cv2.imwrite("uploads/result_single_custom.jpg", frame_drawn_single_custom)
print("结果已保存到 uploads/result_single_custom.jpg")
# 准备标签
label = f"{model_name}:{cls_name} {conf:.2f}"
# 测试多个模型的情况
print("\n=== 测试多个模型 (使用自定义绘制) ===")
frame_drawn_multi, detections_multi = multi_model_inference(
model_list, frame, use_plot_for_single=True # 即使设为True多个模型也会使用自定义绘制
)
print(f"多个模型检测结果:")
for det in detections_multi:
print(f" 模型 {det.model_name}: 检测到 {len(det.boxes)} 个目标")
for box, conf, cls_id, cls_name in zip(det.boxes, det.confidences,
det.class_ids, det.class_names):
print(f"box:{box},conf:{conf},cls_id:{cls_id},cls_name:{cls_name}")
if conf >= 0.25: # 全局阈值
should_draw, color = DetectionVisualizer().should_draw_detection(
cls_id, conf, det.tags)
status = "绘制" if should_draw else "不绘制"
print(f" {cls_name} (置信度: {conf:.2f}): {status}")
# 计算文本尺寸
font = cv2.FONT_HERSHEY_SIMPLEX
font_scale = 0.5
thickness = 2
cv2.imwrite("uploads/result_multi.jpg", frame_drawn_multi)
print("结果已保存到 uploads/result_multi.jpg")
(text_width, text_height), baseline = cv2.getTextSize(
label, font, font_scale, thickness
)
# 绘制标签背景
label_y1 = max(0, y1 - text_height - baseline - 5)
label_y2 = max(0, y1)
label_x2 = x1 + text_width
if label_y1 < label_y2 and label_x2 > x1:
cv2.rectangle(
result_frame,
(x1, label_y1),
(label_x2, label_y2),
color,
-1
)
# 绘制标签文本
text_y = max(baseline + 5, label_y1 + text_height + baseline)
cv2.putText(
result_frame,
label,
(x1, text_y),
font,
font_scale,
(255, 255, 255),
thickness
)
return result_frame
# 简化版本,用于快速集成
def multi_model_inference_simple(
models: List[dict],
frame: np.ndarray,
confidence_threshold: float = 0.25
) -> Tuple[np.ndarray, List[dict]]:
"""
简化的多模型推理串行使用plot绘制
Args:
models: 已加载的模型列表
frame: 视频帧
confidence_threshold: 置信度阈值
Returns:
(绘制完成的帧, 检测结果列表)
"""
if len(models) == 0:
return frame, []
original_frame = frame.copy()
detections = []
# 串行推理
for model_info in models:
try:
model = model_info['model']
conf_thres = model_info.get('conf_thres', confidence_threshold)
iou_thres = model_info.get('iou_thres', 0.45)
imgsz = model_info.get('imgsz', 640)
device = model_info.get('device', 'cpu')
# 执行推理
results = model.predict(
source=original_frame,
imgsz=imgsz,
conf=conf_thres,
iou=iou_thres,
verbose=False,
device=device,
max_det=300
)
result = results[0] if len(results) > 0 else None
model_name = model_info.get('name', f"model_{model_info.get('id', 'unknown')}")
model_id = model_info.get('id', 0)
detection = {
'model_idx': model_id,
'model_name': model_name,
'raw_result': result,
'success': result is not None and result.boxes is not None and len(result.boxes) > 0
}
detections.append(detection)
except Exception as e:
print(f"模型推理失败: {str(e)}")
detections.append({
'model_idx': model_info.get('id', 0),
'model_name': model_info.get('name', 'unknown'),
'raw_result': None,
'success': False
})
# 绘制结果
plotted_frame = original_frame.copy()
if len(models) == 1:
# 单个模型使用plot()
detection = detections[0]
if detection['success'] and detection['raw_result'] is not None:
try:
plotted_frame_rgb = detection['raw_result'].plot(
img=original_frame,
conf=True,
labels=True,
boxes=True
)
if plotted_frame_rgb.shape[-1] == 3:
plotted_frame = cv2.cvtColor(plotted_frame_rgb, cv2.COLOR_RGB2BGR)
except:
plotted_frame = original_frame.copy()
else:
# 多个模型使用自定义绘制
plotted_frame = plot_custom_results(original_frame, detections)
return plotted_frame, detections

View File

@ -78,7 +78,6 @@ def start_cleanup_scheduler():
schedule.run_pending()
except Exception as e:
logger.error(f"定时任务执行失败: {str(e)}")
#time.sleep(60) # 每分钟检查一次
time.sleep(1) # 缩短为1秒检查频率
scheduler_thread = threading.Thread(target=run_scheduler, daemon=True)

View File

@ -109,86 +109,4 @@ def ensure_image_valid(img, default_size=(640, 480)):
cv2.line(img, (w, 0), (0, h), (0, 255, 0), 1)
cv2.circle(img, (w // 2, h // 2), min(w, h) // 4, (255, 0, 0), 2)
return img
# 使用示例
def run_yolo_with_chinese():
# 初始化中文渲染器
renderer = ChineseTextRenderer()
# 模拟YOLO检测结果
# 注意:这里假设你已经有检测结果
detections = [
{"bbox": [100, 100, 200, 300], "conf": 0.95, "class": "person"},
{"bbox": [300, 150, 450, 350], "conf": 0.88, "class": "car"},
]
# 中英文类别映射
class_map = {
"person": "",
"car": "汽车",
"bicycle": "自行车",
"dog": "",
"cat": "",
"chair": "椅子",
"bottle": "瓶子"
}
# 读取图像或创建默认图像
img_path = "test.jpg"
if os.path.exists(img_path):
img = cv2.imread(img_path)
else:
# 用户选择图像文件
import tkinter as tk
from tkinter import filedialog
root = tk.Tk()
root.withdraw()
img_path = filedialog.askopenfilename(
title="选择图像文件",
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.tiff")]
)
if img_path:
img = cv2.imread(img_path)
else:
print("未选择文件,创建测试图像")
img = None
# 确保图像有效
img = ensure_image_valid(img)
# 绘制检测结果
for det in detections:
x1, y1, x2, y2 = det["bbox"]
conf = det["conf"]
cls_en = det["class"]
cls_cn = class_map.get(cls_en, cls_en)
# 绘制边界框
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
# 绘制中文标签
label = f"{cls_cn}: {conf:.2f}"
img = renderer.put_text(img, label, (x1, max(y1 - 20, 10)),
font_size=15, color=(0, 255, 0))
# 添加标题
img = renderer.put_text(img, "YOLO检测结果", (10, 30),
font_size=25, color=(255, 255, 0))
# 显示结果
cv2.imshow("YOLO Detection with Chinese", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 保存结果
output_path = "detection_result.jpg"
cv2.imwrite(output_path, img)
print(f"结果已保存: {output_path}")
if __name__ == "__main__":
run_yolo_with_chinese()
return img

View File

@ -524,7 +524,7 @@ def get_all_tasks():
# 打印tasks内容
import json
print("获取到的任务列表:")
print(json.dumps(tasks, indent=2, ensure_ascii=False))
# print(json.dumps(tasks, indent=2, ensure_ascii=False))
# 增强任务信息
enhanced_tasks = []

20
server_new.py Normal file
View File

@ -0,0 +1,20 @@
import cv2
from ultralytics import YOLO
model = YOLO(r"F:\迅雷下载\yolo26n.pt")
source = "rtmp://175.27.168.120:6019/live/88888"
results = model(source, stream=True, show=False) # 关闭YOLO内置显示
# 遍历结果
for result in results:
# 获取带注释的帧
annotated_frame = result.plot()
# 使用OpenCV显示
cv2.imshow('YOLO Detection', annotated_frame)
# 按'q'键退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 MiB

View File

@ -178,8 +178,8 @@ class TaskManager:
}
}
import json
print(f"获取任务状态 - task_id: {task_id}")
print(f"task_info内容: {json.dumps(task_info, indent=2, ensure_ascii=False, default=str)}")
# print(f"获取任务状态 - task_id: {task_id}")
# print(f"task_info内容: {json.dumps(task_info, indent=2, ensure_ascii=False, default=str)}")
# 更新配置中的任务ID
task_info['config']['task']['taskid'] = task_id
@ -336,8 +336,8 @@ class TaskManager:
# 打印task_info内容
import json
print(f"获取任务状态 - task_id: {task_id}")
print(f"task_info内容: {json.dumps(task_info, indent=2, ensure_ascii=False, default=str)}")
# print(f"获取任务状态 - task_id: {task_id}")
# print(f"task_info内容: {json.dumps(task_info, indent=2, ensure_ascii=False, default=str)}")
# 构建返回数据
result = {

View File

@ -0,0 +1,259 @@
/* 全局样式 */
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 卡片样式 */
.card {
border-radius: 10px;
transition: transform 0.3s, box-shadow 0.3s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
}
/* 统计卡片 */
.stat-card {
border-radius: 8px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #dee2e6;
}
/* 功能图标 */
.feature-icon {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: rgba(13, 110, 253, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
}
/* 图标圆圈 */
.icon-circle {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* 导航栏活动状态指示 */
.nav-link.active {
font-weight: 600;
position: relative;
}
.nav-link.active::after {
content: '';
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 3px;
background-color: #0d6efd;
border-radius: 2px;
}
/* 表格样式 */
.table-hover tbody tr:hover {
background-color: rgba(13, 110, 253, 0.05);
}
/* 视频播放器样式 */
.video-placeholder {
width: 100%;
height: 500px;
background: linear-gradient(135deg, #2c3e50 0%, #4a6491 100%);
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px 10px 0 0;
}
.placeholder-content {
text-align: center;
color: white;
}
.placeholder-content i {
color: rgba(255, 255, 255, 0.7);
}
.simulated-video {
width: 100%;
height: 500px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
position: relative;
overflow: hidden;
border-radius: 10px 10px 0 0;
}
.video-header {
position: absolute;
top: 15px;
left: 15px;
z-index: 10;
}
.detection-box {
position: absolute;
border: 2px solid #0d6efd;
border-radius: 4px;
animation: pulse 2s infinite;
}
.detection-box::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 4px;
animation: pulse-border 2s infinite;
}
.box-label {
position: absolute;
top: -25px;
left: -2px;
background-color: #0d6efd;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
.video-info {
position: absolute;
bottom: 60px;
left: 15px;
display: flex;
gap: 15px;
z-index: 10;
}
.info-item {
display: flex;
align-items: center;
gap: 5px;
color: white;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 4px;
}
.fps-display {
position: absolute;
top: 15px;
right: 15px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
z-index: 10;
}
.video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
}
.controls-bg {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
}
.controls-content {
position: relative;
padding: 15px;
}
/* 动画 */
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(13, 110, 253, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(13, 110, 253, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(13, 110, 253, 0);
}
}
@keyframes pulse-border {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
/* 流列表项 */
.list-group-item.active {
background-color: rgba(13, 110, 253, 0.1);
border-color: rgba(13, 110, 253, 0.2);
color: #0d6efd;
}
/* 按钮样式 */
.btn {
border-radius: 6px;
font-weight: 500;
}
.btn-lg {
padding: 0.75rem 1.5rem;
}
/* 页脚样式 */
footer {
margin-top: auto;
}
/* 响应式调整 */
@media (max-width: 768px) {
.video-placeholder,
.simulated-video {
height: 300px;
}
.video-info {
flex-direction: column;
gap: 5px;
}
.feature-icon {
width: 50px;
height: 50px;
}
.icon-circle {
width: 40px;
height: 40px;
}
}

274
templates/demo/index.html Normal file
View File

@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多任务YOLO检测系统</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.8/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap-icons/1.13.1/font/bootstrap-icons.css">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="bi bi-cpu-fill me-2"></i>多任务YOLO检测系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="index.html">
<i class="bi bi-house-door me-1"></i>首页
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="task-management.html">
<i class="bi bi-list-task me-1"></i>任务管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="monitoring.html">
<i class="bi bi-graph-up me-1"></i>系统监控
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="video-player.html">
<i class="bi bi-camera-video me-1"></i>视频播放
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主内容区 -->
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-8 mx-auto">
<!-- 欢迎卡片 -->
<div class="card shadow-lg border-0 mb-4">
<div class="card-body text-center p-5">
<h1 class="display-4 text-primary mb-3">
<i class="bi bi-cpu-fill"></i> 多任务YOLO检测系统
</h1>
<p class="lead text-muted mb-4">
基于YOLOv8/YOLOv5的多模型并行检测系统支持加密模型、实时推流和任务管理
</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<a href="task-management.html" class="btn btn-primary btn-lg px-4 me-md-2">
<i class="bi bi-play-circle me-2"></i>开始管理任务
</a>
<a href="monitoring.html" class="btn btn-outline-primary btn-lg px-4">
<i class="bi bi-speedometer2 me-2"></i>查看系统状态
</a>
</div>
</div>
</div>
<!-- 功能特性 -->
<div class="row mt-5">
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon mb-3">
<i class="bi bi-shield-lock text-primary" style="font-size: 2.5rem;"></i>
</div>
<h5 class="card-title">加密模型保护</h5>
<p class="card-text text-muted">
所有模型文件强制加密存储确保AI模型安全防止未经授权的使用。
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon mb-3">
<i class="bi bi-layers text-primary" style="font-size: 2.5rem;"></i>
</div>
<h5 class="card-title">多模型并行推理</h5>
<p class="card-text text-muted">
支持多个YOLO模型同时运行每个模型独立配置标签、置信度阈值和绘制颜色。
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="feature-icon mb-3">
<i class="bi bi-broadcast text-primary" style="font-size: 2.5rem;"></i>
</div>
<h5 class="card-title">实时推流与监控</h5>
<p class="card-text text-muted">
支持RTMP/RTSP流输入实时检测结果推流输出提供全面的系统监控。
</p>
</div>
</div>
</div>
</div>
<!-- 快速统计 -->
<div class="card border-0 shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-bar-chart me-2"></i>系统概览</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3 mb-3">
<div class="stat-card p-3 rounded bg-light">
<h2 class="text-primary" id="activeTasks">3</h2>
<p class="text-muted mb-0">运行中任务</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card p-3 rounded bg-light">
<h2 class="text-success" id="totalModels">8</h2>
<p class="text-muted mb-0">已加载模型</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card p-3 rounded bg-light">
<h2 class="text-warning" id="avgFPS">24.5</h2>
<p class="text-muted mb-0">平均FPS</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card p-3 rounded bg-light">
<h2 class="text-info" id="cpuUsage">42%</h2>
<p class="text-muted mb-0">CPU使用率</p>
</div>
</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="row mt-5">
<div class="col-12">
<h4 class="mb-3">快速操作</h4>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-outline-primary" id="simulateCreateTask">
<i class="bi bi-plus-circle me-1"></i>模拟创建任务
</button>
<button class="btn btn-outline-success" id="simulateStartAll">
<i class="bi bi-play me-1"></i>模拟启动所有任务
</button>
<button class="btn btn-outline-danger" id="simulateStopAll">
<i class="bi bi-stop-circle me-1"></i>模拟停止所有任务
</button>
<button class="btn btn-outline-info" id="refreshStats">
<i class="bi bi-arrow-clockwise me-1"></i>刷新统计数据
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<h5>多任务YOLO检测系统</h5>
<p class="text-light">基于YOLOv8/YOLOv5的实时多模型检测平台</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0">© 2023 多任务YOLO检测系统 | 演示版本 v1.0.0</p>
<p class="text-light small">此页面为演示版本,使用模拟数据</p>
</div>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.bundle.min.js"></script>
<script>
// 模拟数据
let stats = {
activeTasks: 3,
totalModels: 8,
avgFPS: 24.5,
cpuUsage: 42
};
// 更新统计数据
function updateStats() {
document.getElementById('activeTasks').textContent = stats.activeTasks;
document.getElementById('totalModels').textContent = stats.totalModels;
document.getElementById('avgFPS').textContent = stats.avgFPS;
document.getElementById('cpuUsage').textContent = stats.cpuUsage + '%';
}
// 模拟创建任务
document.getElementById('simulateCreateTask').addEventListener('click', function() {
stats.activeTasks++;
stats.totalModels += 2;
updateStats();
// 显示成功消息
showNotification('任务创建成功', '成功创建了一个新的检测任务添加了2个模型', 'success');
});
// 模拟启动所有任务
document.getElementById('simulateStartAll').addEventListener('click', function() {
if (stats.activeTasks < 5) {
stats.activeTasks = 5;
updateStats();
showNotification('任务启动成功', '所有任务已成功启动', 'success');
} else {
showNotification('任务已在运行', '所有任务已经在运行中', 'info');
}
});
// 模拟停止所有任务
document.getElementById('simulateStopAll').addEventListener('click', function() {
if (stats.activeTasks > 0) {
stats.activeTasks = 0;
updateStats();
showNotification('任务停止成功', '所有任务已停止', 'warning');
} else {
showNotification('没有运行中的任务', '当前没有运行中的任务', 'info');
}
});
// 刷新统计数据
document.getElementById('refreshStats').addEventListener('click', function() {
// 模拟数据变化
stats.cpuUsage = Math.floor(Math.random() * 30) + 30;
stats.avgFPS = (Math.random() * 10 + 20).toFixed(1);
updateStats();
showNotification('数据已刷新', '系统统计数据已更新', 'info');
});
// 显示通知
function showNotification(title, message, type) {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
notification.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}
// 初始化
updateStats();
</script>
</body>
</html>

View File

@ -0,0 +1,812 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统监控 - 多任务YOLO检测系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="css/style.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.html">
<i class="bi bi-cpu-fill me-2"></i>多任务YOLO检测系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">
<i class="bi bi-house-door me-1"></i>首页
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="task-management.html">
<i class="bi bi-list-task me-1"></i>任务管理
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="monitoring.html">
<i class="bi bi-graph-up me-1"></i>系统监控
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="video-player.html">
<i class="bi bi-camera-video me-1"></i>视频播放
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主内容区 -->
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-graph-up text-primary me-2"></i>系统监控</h2>
<div>
<button class="btn btn-outline-primary me-2" id="refreshCharts">
<i class="bi bi-arrow-clockwise me-1"></i>刷新数据
</button>
<div class="btn-group">
<button class="btn btn-outline-secondary" id="autoRefreshToggle">
<i class="bi bi-play-circle me-1"></i>自动刷新
</button>
</div>
</div>
</div>
<!-- 系统资源卡片 -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">CPU使用率</h6>
<h2 id="cpuPercent">42%</h2>
</div>
<div class="icon-circle bg-danger">
<i class="bi bi-cpu text-white"></i>
</div>
</div>
<div class="progress mt-2" style="height: 6px;">
<div class="progress-bar bg-danger" id="cpuBar" style="width: 42%"></div>
</div>
<p class="text-muted small mt-2 mb-0">总核心: 8, 使用中: 3</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">内存使用</h6>
<h2 id="memoryPercent">68%</h2>
</div>
<div class="icon-circle bg-info">
<i class="bi bi-memory text-white"></i>
</div>
</div>
<div class="progress mt-2" style="height: 6px;">
<div class="progress-bar bg-info" id="memoryBar" style="width: 68%"></div>
</div>
<p class="text-muted small mt-2 mb-0">已用: 10.2GB / 总: 16GB</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">GPU使用率</h6>
<h2 id="gpuPercent">78%</h2>
</div>
<div class="icon-circle bg-warning">
<i class="bi bi-gpu-card text-white"></i>
</div>
</div>
<div class="progress mt-2" style="height: 6px;">
<div class="progress-bar bg-warning" id="gpuBar" style="width: 78%"></div>
</div>
<p class="text-muted small mt-2 mb-0">显存: 6.4GB / 总: 8GB</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">磁盘使用</h6>
<h2 id="diskPercent">34%</h2>
</div>
<div class="icon-circle bg-success">
<i class="bi bi-hdd text-white"></i>
</div>
</div>
<div class="progress mt-2" style="height: 6px;">
<div class="progress-bar bg-success" id="diskBar" style="width: 34%"></div>
</div>
<p class="text-muted small mt-2 mb-0">可用: 256GB / 总: 512GB</p>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="row mb-4">
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">CPU使用率趋势</h5>
</div>
<div class="card-body">
<canvas id="cpuChart" height="250"></canvas>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">内存使用趋势</h5>
</div>
<div class="card-body">
<canvas id="memoryChart" height="250"></canvas>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">任务FPS分布</h5>
</div>
<div class="card-body">
<canvas id="fpsChart" height="250"></canvas>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">模型使用统计</h5>
</div>
<div class="card-body">
<canvas id="modelChart" height="250"></canvas>
</div>
</div>
</div>
</div>
<!-- 任务性能表 -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">任务性能监控</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>任务名称</th>
<th>状态</th>
<th>FPS</th>
<th>处理时间</th>
<th>检测数</th>
<th>模型数</th>
<th>GPU显存</th>
<th>运行时长</th>
</tr>
</thead>
<tbody id="performanceTable">
<!-- 性能数据将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
</div>
</div>
<!-- 系统信息 -->
<div class="row">
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">系统信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<td><strong>操作系统</strong></td>
<td>Ubuntu 20.04 LTS</td>
</tr>
<tr>
<td><strong>Python版本</strong></td>
<td>3.8.10</td>
</tr>
<tr>
<td><strong>PyTorch版本</strong></td>
<td>2.0.1+cu118</td>
</tr>
<tr>
<td><strong>CUDA版本</strong></td>
<td>11.8</td>
</tr>
<tr>
<td><strong>Ultralytics版本</strong></td>
<td>8.0.0</td>
</tr>
<tr>
<td><strong>OpenCV版本</strong></td>
<td>4.8.0</td>
</tr>
<tr>
<td><strong>FFmpeg版本</strong></td>
<td>4.4.2</td>
</tr>
<tr>
<td><strong>启动时间</strong></td>
<td id="systemUptime">2天 5小时 30分钟</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">硬件信息</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<td><strong>CPU型号</strong></td>
<td>Intel Core i9-13900K</td>
</tr>
<tr>
<td><strong>CPU核心数</strong></td>
<td>24核心 (8P+16E)</td>
</tr>
<tr>
<td><strong>GPU型号</strong></td>
<td>NVIDIA RTX 4090</td>
</tr>
<tr>
<td><strong>GPU显存</strong></td>
<td>24GB GDDR6X</td>
</tr>
<tr>
<td><strong>系统内存</strong></td>
<td>32GB DDR5 5600MHz</td>
</tr>
<tr>
<td><strong>磁盘型号</strong></td>
<td>Samsung 980 Pro 1TB NVMe</td>
</tr>
<tr>
<td><strong>网络接口</strong></td>
<td>2.5Gbps Ethernet</td>
</tr>
<tr>
<td><strong>系统温度</strong></td>
<td id="systemTemp">42°C</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<h5>系统监控</h5>
<p class="text-light">多任务YOLO检测系统的实时监控界面</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0">© 2023 多任务YOLO检测系统 | 演示版本 v1.0.0</p>
<p class="text-light small">此页面为演示版本,使用模拟数据</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 图表实例
let cpuChart, memoryChart, fpsChart, modelChart;
let autoRefreshInterval = null;
let isAutoRefreshing = false;
// 模拟性能数据
const mockPerformanceData = [
{ name: "交通监控-路口A", status: "running", fps: 24.5, processTime: "45ms", detections: 156, models: 3, gpuMem: "2.4GB", uptime: "5h 30m" },
{ name: "安全监控-入口", status: "running", fps: 28.1, processTime: "38ms", detections: 89, models: 2, gpuMem: "1.8GB", uptime: "4h 15m" },
{ name: "停车场监控", status: "stopped", fps: 0, processTime: "0ms", detections: 0, models: 1, gpuMem: "0GB", uptime: "0m" },
{ name: "生产线检测", status: "error", fps: 15.3, processTime: "62ms", detections: 0, models: 2, gpuMem: "1.2GB", uptime: "2h 45m" },
{ name: "测试任务", status: "creating", fps: 0, processTime: "0ms", detections: 0, models: 1, gpuMem: "0GB", uptime: "5m" }
];
// 模拟图表数据
let timeLabels = [];
let cpuData = [];
let memoryData = [];
let gpuData = [];
// 初始化时间标签
function initTimeLabels() {
timeLabels = [];
const now = new Date();
for (let i = 9; i >= 0; i--) {
const time = new Date(now.getTime() - i * 60000);
timeLabels.push(time.getHours().toString().padStart(2, '0') + ':' + time.getMinutes().toString().padStart(2, '0'));
}
}
// 初始化数据
function initChartData() {
cpuData = [];
memoryData = [];
gpuData = [];
for (let i = 0; i < 10; i++) {
cpuData.push(Math.floor(Math.random() * 30) + 30);
memoryData.push(Math.floor(Math.random() * 20) + 50);
gpuData.push(Math.floor(Math.random() * 30) + 50);
}
}
// 初始化CPU图表
function initCpuChart() {
const ctx = document.getElementById('cpuChart').getContext('2d');
if (cpuChart) {
cpuChart.destroy();
}
cpuChart = new Chart(ctx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: 'CPU使用率 (%)',
data: cpuData,
borderColor: '#dc3545',
backgroundColor: 'rgba(220, 53, 69, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化内存图表
function initMemoryChart() {
const ctx = document.getElementById('memoryChart').getContext('2d');
if (memoryChart) {
memoryChart.destroy();
}
memoryChart = new Chart(ctx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [{
label: '内存使用率 (%)',
data: memoryData,
borderColor: '#0dcaf0',
backgroundColor: 'rgba(13, 202, 240, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
}
}
});
}
// 初始化FPS图表
function initFpsChart() {
const ctx = document.getElementById('fpsChart').getContext('2d');
if (fpsChart) {
fpsChart.destroy();
}
// 获取运行中任务的FPS数据
const runningTasks = mockPerformanceData.filter(task => task.status === 'running');
const taskNames = runningTasks.map(task => task.name);
const taskFps = runningTasks.map(task => task.fps);
fpsChart = new Chart(ctx, {
type: 'bar',
data: {
labels: taskNames,
datasets: [{
label: 'FPS',
data: taskFps,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'FPS'
}
}
}
}
});
}
// 初始化模型图表
function initModelChart() {
const ctx = document.getElementById('modelChart').getContext('2d');
if (modelChart) {
modelChart.destroy();
}
// 模拟模型使用数据
const modelNames = ['yolov8n.pt', 'yolov8s.pt', 'yolov8m.pt', 'custom.pt', 'yolov5s.pt'];
const modelUsage = [45, 30, 15, 7, 3];
modelChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: modelNames,
datasets: [{
data: modelUsage,
backgroundColor: [
'rgba(255, 99, 132, 0.7)',
'rgba(54, 162, 235, 0.7)',
'rgba(255, 206, 86, 0.7)',
'rgba(75, 192, 192, 0.7)',
'rgba(153, 102, 255, 0.7)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right'
}
}
}
});
}
// 渲染性能表格
function renderPerformanceTable() {
const tbody = document.getElementById('performanceTable');
tbody.innerHTML = '';
mockPerformanceData.forEach(task => {
const row = document.createElement('tr');
// 状态颜色
let statusColor = 'secondary';
let statusText = task.status;
if (task.status === 'running') {
statusColor = 'success';
statusText = '运行中';
} else if (task.status === 'error') {
statusColor = 'danger';
statusText = '错误';
} else if (task.status === 'creating') {
statusColor = 'info';
statusText = '创建中';
} else if (task.status === 'stopped') {
statusColor = 'secondary';
statusText = '已停止';
}
row.innerHTML = `
<td><strong>${task.name}</strong></td>
<td><span class="badge bg-${statusColor}">${statusText}</span></td>
<td>${task.fps > 0 ? task.fps.toFixed(1) : '0'}</td>
<td>${task.processTime}</td>
<td>${task.detections}</td>
<td>${task.models}</td>
<td>${task.gpuMem}</td>
<td>${task.uptime}</td>
`;
tbody.appendChild(row);
});
}
// 更新系统资源数据
function updateResourceData() {
// 更新资源百分比
const cpuPercent = Math.floor(Math.random() * 30) + 30;
const memoryPercent = Math.floor(Math.random() * 20) + 50;
const gpuPercent = Math.floor(Math.random() * 30) + 50;
const diskPercent = Math.floor(Math.random() * 20) + 30;
document.getElementById('cpuPercent').textContent = cpuPercent + '%';
document.getElementById('cpuBar').style.width = cpuPercent + '%';
document.getElementById('memoryPercent').textContent = memoryPercent + '%';
document.getElementById('memoryBar').style.width = memoryPercent + '%';
document.getElementById('gpuPercent').textContent = gpuPercent + '%';
document.getElementById('gpuBar').style.width = gpuPercent + '%';
document.getElementById('diskPercent').textContent = diskPercent + '%';
document.getElementById('diskBar').style.width = diskPercent + '%';
// 更新系统温度和运行时间
const temp = Math.floor(Math.random() * 10) + 38;
document.getElementById('systemTemp').textContent = temp + '°C';
// 更新图表数据
updateChartData();
// 更新性能数据
updatePerformanceData();
// 更新运行时间
updateUptime();
}
// 更新图表数据
function updateChartData() {
// 移除第一个数据点,添加新的数据点
cpuData.shift();
memoryData.shift();
gpuData.shift();
// 添加新的数据点
cpuData.push(Math.floor(Math.random() * 30) + 30);
memoryData.push(Math.floor(Math.random() * 20) + 50);
gpuData.push(Math.floor(Math.random() * 30) + 50);
// 更新时间标签
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
timeLabels.shift();
timeLabels.push(timeStr);
// 更新图表
cpuChart.update();
memoryChart.update();
fpsChart.update();
}
// 更新性能数据
function updatePerformanceData() {
mockPerformanceData.forEach(task => {
if (task.status === 'running') {
// 随机更新FPS
task.fps = (Math.random() * 10 + 20).toFixed(1);
// 增加检测数量
task.detections += Math.floor(Math.random() * 10);
// 更新运行时间
if (task.uptime.includes('h')) {
const hours = parseInt(task.uptime.split('h')[0]);
const minutes = parseInt(task.uptime.split('h')[1].split('m')[0]);
let newMinutes = minutes + 1;
let newHours = hours;
if (newMinutes >= 60) {
newHours++;
newMinutes = 0;
}
task.uptime = newHours + 'h ' + newMinutes + 'm';
} else if (task.uptime.includes('m')) {
const minutes = parseInt(task.uptime.split('m')[0]);
if (minutes >= 60) {
task.uptime = '1h ' + (minutes - 60) + 'm';
} else {
task.uptime = (minutes + 1) + 'm';
}
}
}
});
renderPerformanceTable();
initFpsChart(); // 重新初始化FPS图表以更新数据
}
// 更新运行时间
function updateUptime() {
const uptimeElement = document.getElementById('systemUptime');
let text = uptimeElement.textContent;
if (text.includes('天')) {
const days = parseInt(text.split('天')[0]);
const hours = parseInt(text.split('天')[1].split('小时')[0]);
const minutes = parseInt(text.split('小时')[1].split('分钟')[0]);
let newMinutes = minutes + 1;
let newHours = hours;
let newDays = days;
if (newMinutes >= 60) {
newHours++;
newMinutes = 0;
}
if (newHours >= 24) {
newDays++;
newHours = 0;
}
uptimeElement.textContent = newDays + '天 ' + newHours + '小时 ' + newMinutes + '分钟';
}
}
// 切换自动刷新
function toggleAutoRefresh() {
const button = document.getElementById('autoRefreshToggle');
const icon = button.querySelector('i');
if (isAutoRefreshing) {
// 停止自动刷新
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
button.innerHTML = '<i class="bi bi-play-circle me-1"></i>自动刷新';
button.classList.remove('btn-primary');
button.classList.add('btn-outline-secondary');
isAutoRefreshing = false;
showNotification('自动刷新已停止', '系统将不再自动更新数据', 'info');
} else {
// 开始自动刷新
autoRefreshInterval = setInterval(updateResourceData, 5000);
button.innerHTML = '<i class="bi bi-pause-circle me-1"></i>停止自动刷新';
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-primary');
isAutoRefreshing = true;
showNotification('自动刷新已启动', '系统将每5秒自动更新数据', 'success');
}
}
// 显示通知
function showNotification(title, message, type) {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
notification.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 初始化数据
initTimeLabels();
initChartData();
// 初始化图表
initCpuChart();
initMemoryChart();
initFpsChart();
initModelChart();
// 渲染性能表格
renderPerformanceTable();
// 更新资源数据
updateResourceData();
// 绑定事件
document.getElementById('refreshCharts').addEventListener('click', updateResourceData);
document.getElementById('autoRefreshToggle').addEventListener('click', toggleAutoRefresh);
// 显示欢迎消息
setTimeout(() => {
showNotification('监控系统已就绪', '系统监控数据已加载完成', 'success');
}, 1000);
});
</script>
</body>
</html>

View File

@ -0,0 +1,765 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务管理 - 多任务YOLO检测系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.html">
<i class="bi bi-cpu-fill me-2"></i>多任务YOLO检测系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">
<i class="bi bi-house-door me-1"></i>首页
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="task-management.html">
<i class="bi bi-list-task me-1"></i>任务管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="monitoring.html">
<i class="bi bi-graph-up me-1"></i>系统监控
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="video-player.html">
<i class="bi bi-camera-video me-1"></i>视频播放
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主内容区 -->
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-list-task text-primary me-2"></i>任务管理</h2>
<div>
<button class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#createTaskModal">
<i class="bi bi-plus-circle me-1"></i>创建新任务
</button>
<button class="btn btn-outline-secondary" id="refreshTasks">
<i class="bi bi-arrow-clockwise me-1"></i>刷新
</button>
</div>
</div>
<!-- 任务列表 -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white">
<h5 class="mb-0">运行中的任务</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>任务ID</th>
<th>任务名称</th>
<th>RTMP地址</th>
<th>模型数量</th>
<th>状态</th>
<th>FPS</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="taskTableBody">
<!-- 任务数据将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
</div>
<div class="card-footer text-muted">
<div class="d-flex justify-content-between align-items-center">
<div><span id="totalTasks">0</span> 个任务,<span id="runningTasks">0</span> 个运行中</div>
<div class="small">最后更新: <span id="lastUpdateTime">--:--:--</span></div>
</div>
</div>
</div>
<!-- 批量操作 -->
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white">
<h5 class="mb-0">批量操作</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-success" id="startAllTasks">
<i class="bi bi-play-circle me-1"></i>启动所有任务
</button>
<button class="btn btn-warning" id="stopAllTasks">
<i class="bi bi-pause-circle me-1"></i>停止所有任务
</button>
<button class="btn btn-danger" id="cleanupStoppedTasks">
<i class="bi bi-trash me-1"></i>清理已停止任务
</button>
<button class="btn btn-info" id="cleanupAllTasks">
<i class="bi bi-eraser me-1"></i>清理所有任务
</button>
</div>
</div>
</div>
<!-- 模型库 -->
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">加密模型库</h5>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#uploadModelModal">
<i class="bi bi-upload me-1"></i>上传模型
</button>
</div>
<div class="card-body">
<div class="row" id="modelLibrary">
<!-- 模型卡片将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 创建任务模态框 -->
<div class="modal fade" id="createTaskModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">创建新检测任务</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createTaskForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="taskName" class="form-label">任务名称</label>
<input type="text" class="form-control" id="taskName" placeholder="输入任务名称" required>
</div>
<div class="col-md-6 mb-3">
<label for="rtmpUrl" class="form-label">RTMP视频流地址</label>
<input type="text" class="form-control" id="rtmpUrl" placeholder="rtmp://example.com/live/stream" required>
</div>
</div>
<div class="mb-3">
<label for="pushUrl" class="form-label">推流地址 (可选)</label>
<input type="text" class="form-control" id="pushUrl" placeholder="rtmp://example.com/live/output">
</div>
<div class="mb-3">
<label class="form-label">选择模型</label>
<div class="border rounded p-3" style="max-height: 300px; overflow-y: auto;">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="model1" checked>
<label class="form-check-label" for="model1">
yolov8n.pt (行人检测)
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="model2" checked>
<label class="form-check-label" for="model2">
yolov8s.pt (车辆检测)
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="model3">
<label class="form-check-label" for="model3">
yolov8m.pt (交通标志识别)
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="model4">
<label class="form-check-label" for="model4">
custom.pt (自定义模型)
</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="encryptionKey" class="form-label">加密密钥</label>
<input type="password" class="form-control" id="encryptionKey" placeholder="输入模型加密密钥" required>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enablePush" checked>
<label class="form-check-label" for="enablePush">
启用结果推流
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="submitTaskForm">创建任务</button>
</div>
</div>
</div>
</div>
<!-- 上传模型模态框 -->
<div class="modal fade" id="uploadModelModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">上传加密模型</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="uploadModelForm">
<div class="mb-3">
<label for="modelFile" class="form-label">选择模型文件 (.pt, .pth)</label>
<input class="form-control" type="file" id="modelFile" accept=".pt,.pth,.enc">
</div>
<div class="mb-3">
<label for="modelName" class="form-label">模型名称</label>
<input type="text" class="form-control" id="modelName" placeholder="输入模型名称">
</div>
<div class="mb-3">
<label for="modelDescription" class="form-label">模型描述</label>
<textarea class="form-control" id="modelDescription" rows="3" placeholder="描述模型的用途和特点"></textarea>
</div>
<div class="mb-3">
<label for="modelEncryptionKey" class="form-label">加密密钥</label>
<input type="password" class="form-control" id="modelEncryptionKey" placeholder="输入加密密钥" required>
</div>
<div class="mb-3">
<label for="confirmEncryptionKey" class="form-label">确认加密密钥</label>
<input type="password" class="form-control" id="confirmEncryptionKey" placeholder="再次输入加密密钥" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="submitUploadModel">上传模型</button>
</div>
</div>
</div>
</div>
<!-- 任务详情模态框 -->
<div class="modal fade" id="taskDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailModalTitle">任务详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="taskDetailContent">
<!-- 任务详情将通过JavaScript动态生成 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<h5>任务管理系统</h5>
<p class="text-light">多任务YOLO检测系统的任务管理界面</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0">© 2023 多任务YOLO检测系统 | 演示版本 v1.0.0</p>
<p class="text-light small">此页面为演示版本,使用模拟数据</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 模拟任务数据
const mockTasks = [
{
id: "task_001",
name: "交通监控-路口A",
rtmpUrl: "rtmp://localhost:1935/live/camera1",
pushUrl: "rtmp://localhost:1935/live/output1",
models: 3,
status: "running",
fps: 24.5,
createdAt: "2023-10-15 09:30:00",
detectionCount: 156,
avgProcessTime: "0.045s",
encryptionStatus: "已验证"
},
{
id: "task_002",
name: "安全监控-入口",
rtmpUrl: "rtmp://localhost:1935/live/camera2",
pushUrl: "rtmp://localhost:1935/live/output2",
models: 2,
status: "running",
fps: 28.1,
createdAt: "2023-10-15 10:15:00",
detectionCount: 89,
avgProcessTime: "0.038s",
encryptionStatus: "已验证"
},
{
id: "task_003",
name: "停车场监控",
rtmpUrl: "rtmp://localhost:1935/live/camera3",
pushUrl: "",
models: 1,
status: "stopped",
fps: 0,
createdAt: "2023-10-14 14:20:00",
detectionCount: 324,
avgProcessTime: "0.042s",
encryptionStatus: "已验证"
},
{
id: "task_004",
name: "生产线检测",
rtmpUrl: "rtmp://192.168.1.100:1935/live/production",
pushUrl: "rtmp://192.168.1.100:1935/live/production_out",
models: 2,
status: "error",
fps: 15.3,
createdAt: "2023-10-15 08:45:00",
detectionCount: 0,
avgProcessTime: "0.062s",
encryptionStatus: "已验证"
},
{
id: "task_005",
name: "测试任务",
rtmpUrl: "rtmp://test.com:1935/live/test",
pushUrl: "",
models: 1,
status: "creating",
fps: 0,
createdAt: "2023-10-15 11:05:00",
detectionCount: 0,
avgProcessTime: "0s",
encryptionStatus: "验证中"
}
];
// 模拟模型数据
const mockModels = [
{ id: 1, name: "yolov8n.pt", size: "6.2 MB", type: "行人检测", encrypted: true, uploaded: "2023-10-10" },
{ id: 2, name: "yolov8s.pt", size: "22.4 MB", type: "车辆检测", encrypted: true, uploaded: "2023-10-11" },
{ id: 3, name: "yolov8m.pt", size: "50.1 MB", type: "交通标志", encrypted: true, uploaded: "2023-10-12" },
{ id: 4, name: "custom.pt", size: "15.7 MB", type: "自定义模型", encrypted: true, uploaded: "2023-10-13" },
{ id: 5, name: "yolov5s.pt", size: "14.5 MB", type: "通用检测", encrypted: true, uploaded: "2023-10-14" }
];
// 状态颜色映射
const statusColors = {
running: "success",
stopped: "secondary",
error: "danger",
creating: "info"
};
// 状态文本映射
const statusText = {
running: "运行中",
stopped: "已停止",
error: "错误",
creating: "创建中"
};
// 渲染任务表格
function renderTaskTable() {
const tbody = document.getElementById('taskTableBody');
tbody.innerHTML = '';
let runningCount = 0;
mockTasks.forEach(task => {
const row = document.createElement('tr');
const statusColor = statusColors[task.status] || 'secondary';
const statusTextValue = statusText[task.status] || task.status;
if (task.status === 'running') runningCount++;
row.innerHTML = `
<td><code>${task.id}</code></td>
<td><strong>${task.name}</strong></td>
<td><small>${task.rtmpUrl}</small></td>
<td>${task.models}</td>
<td><span class="badge bg-${statusColor}">${statusTextValue}</span></td>
<td>${task.fps > 0 ? task.fps.toFixed(1) : '0'}</td>
<td>${task.createdAt}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1 view-task" data-task-id="${task.id}">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-success me-1 start-task" data-task-id="${task.id}" ${task.status === 'running' ? 'disabled' : ''}>
<i class="bi bi-play"></i>
</button>
<button class="btn btn-sm btn-outline-warning me-1 stop-task" data-task-id="${task.id}" ${task.status !== 'running' ? 'disabled' : ''}>
<i class="bi bi-stop"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-task" data-task-id="${task.id}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
// 更新统计信息
document.getElementById('totalTasks').textContent = mockTasks.length;
document.getElementById('runningTasks').textContent = runningCount;
document.getElementById('lastUpdateTime').textContent = new Date().toLocaleTimeString();
// 添加事件监听器
addTaskEventListeners();
}
// 渲染模型库
function renderModelLibrary() {
const container = document.getElementById('modelLibrary');
container.innerHTML = '';
mockModels.forEach(model => {
const col = document.createElement('div');
col.className = 'col-md-4 mb-3';
col.innerHTML = `
<div class="card h-100">
<div class="card-body">
<h6 class="card-title">${model.name}</h6>
<p class="card-text small text-muted">
<i class="bi bi-info-circle me-1"></i>${model.type}<br>
<i class="bi bi-database me-1"></i>${model.size}<br>
<i class="bi bi-calendar me-1"></i>${model.uploaded}
</p>
<div class="d-flex justify-content-between">
<span class="badge bg-success"><i class="bi bi-shield-check me-1"></i>已加密</span>
<div>
<button class="btn btn-sm btn-outline-primary me-1" title="查看详情">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-danger" title="删除模型">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(col);
});
}
// 添加任务事件监听器
function addTaskEventListeners() {
// 查看任务详情
document.querySelectorAll('.view-task').forEach(button => {
button.addEventListener('click', function() {
const taskId = this.getAttribute('data-task-id');
const task = mockTasks.find(t => t.id === taskId);
showTaskDetail(task);
});
});
// 启动任务
document.querySelectorAll('.start-task').forEach(button => {
button.addEventListener('click', function() {
const taskId = this.getAttribute('data-task-id');
startTask(taskId);
});
});
// 停止任务
document.querySelectorAll('.stop-task').forEach(button => {
button.addEventListener('click', function() {
const taskId = this.getAttribute('data-task-id');
stopTask(taskId);
});
});
// 删除任务
document.querySelectorAll('.delete-task').forEach(button => {
button.addEventListener('click', function() {
const taskId = this.getAttribute('data-task-id');
deleteTask(taskId);
});
});
}
// 显示任务详情
function showTaskDetail(task) {
const modalTitle = document.getElementById('detailModalTitle');
const modalContent = document.getElementById('taskDetailContent');
modalTitle.textContent = `任务详情: ${task.name}`;
const statusColor = statusColors[task.status] || 'secondary';
const statusTextValue = statusText[task.status] || task.status;
modalContent.innerHTML = `
<div class="row">
<div class="col-md-6">
<h6>基本信息</h6>
<table class="table table-sm">
<tr><td><strong>任务ID:</strong></td><td>${task.id}</td></tr>
<tr><td><strong>任务名称:</strong></td><td>${task.name}</td></tr>
<tr><td><strong>状态:</strong></td><td><span class="badge bg-${statusColor}">${statusTextValue}</span></td></tr>
<tr><td><strong>创建时间:</strong></td><td>${task.createdAt}</td></tr>
<tr><td><strong>加密状态:</strong></td><td><span class="badge bg-success">${task.encryptionStatus}</span></td></tr>
</table>
</div>
<div class="col-md-6">
<h6>性能统计</h6>
<table class="table table-sm">
<tr><td><strong>模型数量:</strong></td><td>${task.models}</td></tr>
<tr><td><strong>当前FPS:</strong></td><td>${task.fps > 0 ? task.fps.toFixed(1) : '0'}</td></tr>
<tr><td><strong>平均处理时间:</strong></td><td>${task.avgProcessTime}</td></tr>
<tr><td><strong>检测目标数:</strong></td><td>${task.detectionCount}</td></tr>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>流地址</h6>
<div class="mb-2">
<strong>RTMP输入:</strong> <code>${task.rtmpUrl}</code>
</div>
${task.pushUrl ? `<div><strong>推流输出:</strong> <code>${task.pushUrl}</code></div>` : ''}
</div>
</div>
`;
const modal = new bootstrap.Modal(document.getElementById('taskDetailModal'));
modal.show();
}
// 启动任务
function startTask(taskId) {
const task = mockTasks.find(t => t.id === taskId);
if (task) {
task.status = 'running';
task.fps = 24.5;
renderTaskTable();
showNotification('任务启动成功', `任务 ${task.name} 已成功启动`, 'success');
}
}
// 停止任务
function stopTask(taskId) {
const task = mockTasks.find(t => t.id === taskId);
if (task) {
task.status = 'stopped';
task.fps = 0;
renderTaskTable();
showNotification('任务停止成功', `任务 ${task.name} 已停止`, 'warning');
}
}
// 删除任务
function deleteTask(taskId) {
if (confirm('确定要删除这个任务吗?')) {
const index = mockTasks.findIndex(t => t.id === taskId);
if (index !== -1) {
mockTasks.splice(index, 1);
renderTaskTable();
showNotification('任务删除成功', '任务已成功删除', 'danger');
}
}
}
// 批量操作
document.getElementById('startAllTasks').addEventListener('click', function() {
mockTasks.forEach(task => {
task.status = 'running';
task.fps = task.fps > 0 ? task.fps : 24.5;
});
renderTaskTable();
showNotification('批量启动成功', '所有任务已启动', 'success');
});
document.getElementById('stopAllTasks').addEventListener('click', function() {
mockTasks.forEach(task => {
task.status = 'stopped';
task.fps = 0;
});
renderTaskTable();
showNotification('批量停止成功', '所有任务已停止', 'warning');
});
document.getElementById('cleanupStoppedTasks').addEventListener('click', function() {
const stoppedTasks = mockTasks.filter(task => task.status === 'stopped');
const stoppedCount = stoppedTasks.length;
stoppedTasks.forEach(task => {
const index = mockTasks.findIndex(t => t.id === task.id);
if (index !== -1) {
mockTasks.splice(index, 1);
}
});
renderTaskTable();
showNotification('清理完成', `已清理 ${stoppedCount} 个已停止的任务`, 'info');
});
document.getElementById('cleanupAllTasks').addEventListener('click', function() {
if (confirm('确定要清理所有任务吗?这将删除所有任务记录。')) {
mockTasks.length = 0;
renderTaskTable();
showNotification('清理完成', '所有任务已清理', 'info');
}
});
// 创建任务表单提交
document.getElementById('submitTaskForm').addEventListener('click', function() {
const taskName = document.getElementById('taskName').value;
const rtmpUrl = document.getElementById('rtmpUrl').value;
const encryptionKey = document.getElementById('encryptionKey').value;
if (!taskName || !rtmpUrl || !encryptionKey) {
showNotification('表单验证失败', '请填写所有必填字段', 'danger');
return;
}
// 生成新任务
const newTask = {
id: `task_${String(mockTasks.length + 1).padStart(3, '0')}`,
name: taskName,
rtmpUrl: rtmpUrl,
pushUrl: document.getElementById('pushUrl').value,
models: document.querySelectorAll('input[type="checkbox"]:checked').length,
status: 'creating',
fps: 0,
createdAt: new Date().toLocaleString(),
detectionCount: 0,
avgProcessTime: "0s",
encryptionStatus: "验证中"
};
mockTasks.push(newTask);
renderTaskTable();
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('createTaskModal')).hide();
// 重置表单
document.getElementById('createTaskForm').reset();
// 模拟任务创建过程
setTimeout(() => {
const taskIndex = mockTasks.findIndex(t => t.id === newTask.id);
if (taskIndex !== -1) {
mockTasks[taskIndex].status = 'running';
mockTasks[taskIndex].fps = 24.5;
mockTasks[taskIndex].encryptionStatus = '已验证';
renderTaskTable();
}
}, 1500);
showNotification('任务创建成功', `任务 ${taskName} 已创建并启动`, 'success');
});
// 上传模型表单提交
document.getElementById('submitUploadModel').addEventListener('click', function() {
const modelName = document.getElementById('modelName').value;
const encryptionKey = document.getElementById('modelEncryptionKey').value;
const confirmKey = document.getElementById('confirmEncryptionKey').value;
if (!modelName || !encryptionKey || !confirmKey) {
showNotification('表单验证失败', '请填写所有必填字段', 'danger');
return;
}
if (encryptionKey !== confirmKey) {
showNotification('密钥不匹配', '两次输入的加密密钥不一致', 'danger');
return;
}
// 添加新模型
const newModel = {
id: mockModels.length + 1,
name: modelName || '未命名模型.pt',
size: `${(Math.random() * 50 + 5).toFixed(1)} MB`,
type: document.getElementById('modelDescription').value || '自定义模型',
encrypted: true,
uploaded: new Date().toLocaleDateString()
};
mockModels.push(newModel);
renderModelLibrary();
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('uploadModelModal')).hide();
// 重置表单
document.getElementById('uploadModelForm').reset();
showNotification('模型上传成功', `模型 ${newModel.name} 已上传并加密`, 'success');
});
// 刷新任务列表
document.getElementById('refreshTasks').addEventListener('click', function() {
// 模拟数据更新
mockTasks.forEach(task => {
if (task.status === 'running') {
task.fps = (Math.random() * 10 + 20).toFixed(1);
task.detectionCount += Math.floor(Math.random() * 10);
}
});
renderTaskTable();
showNotification('数据已刷新', '任务列表已更新', 'info');
});
// 显示通知
function showNotification(title, message, type) {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
notification.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
renderTaskTable();
renderModelLibrary();
});
</script>
</body>
</html>

View File

@ -0,0 +1,816 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频播放 - 多任务YOLO检测系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="index.html">
<i class="bi bi-cpu-fill me-2"></i>多任务YOLO检测系统
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="index.html">
<i class="bi bi-house-door me-1"></i>首页
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="task-management.html">
<i class="bi bi-list-task me-1"></i>任务管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="monitoring.html">
<i class="bi bi-graph-up me-1"></i>系统监控
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="video-player.html">
<i class="bi bi-camera-video me-1"></i>视频播放
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 主内容区 -->
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-camera-video text-primary me-2"></i>视频播放</h2>
<div>
<button class="btn btn-outline-primary me-2" id="refreshStreams">
<i class="bi bi-arrow-clockwise me-1"></i>刷新流列表
</button>
<div class="btn-group">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addStreamModal">
<i class="bi bi-plus-circle me-1"></i>添加视频流
</button>
</div>
</div>
</div>
<!-- 视频播放区域 -->
<div class="row">
<div class="col-lg-8 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="currentStreamTitle">交通监控-路口A</h5>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" id="fullscreenBtn">
<i class="bi bi-arrows-fullscreen"></i>
</button>
<button class="btn btn-sm btn-outline-secondary" id="screenshotBtn">
<i class="bi bi-camera"></i>
</button>
</div>
</div>
<div class="card-body p-0 position-relative" id="videoContainer">
<!-- 视频占位图 -->
<div class="video-placeholder" id="videoPlaceholder">
<div class="placeholder-content">
<i class="bi bi-play-circle" style="font-size: 4rem;"></i>
<p class="mt-3">选择左侧的视频流开始播放</p>
<p class="small text-muted">支持RTMP/RTSP/HTTP-FLV流</p>
</div>
</div>
<!-- 模拟视频 -->
<div class="simulated-video" id="simulatedVideo" style="display: none;">
<div class="video-header">
<span class="badge bg-danger me-2">LIVE</span>
<span class="text-light">1080p • 24 FPS</span>
</div>
<!-- 模拟检测框 -->
<div class="detection-box" style="top: 30%; left: 40%; width: 120px; height: 200px;">
<div class="box-label">人 0.92</div>
</div>
<div class="detection-box" style="top: 35%; left: 60%; width: 180px; height: 90px;">
<div class="box-label">汽车 0.87</div>
</div>
<div class="detection-box" style="top: 60%; left: 20%; width: 100px; height: 150px;">
<div class="box-label">自行车 0.78</div>
</div>
<div class="detection-box" style="top: 50%; left: 70%; width: 140px; height: 80px;">
<div class="box-label">卡车 0.81</div>
</div>
<!-- 视频信息 -->
<div class="video-info">
<div class="info-item">
<i class="bi bi-clock"></i>
<span>12:45:23</span>
</div>
<div class="info-item">
<i class="bi bi-geo-alt"></i>
<span>路口A</span>
</div>
<div class="info-item">
<i class="bi bi-cpu"></i>
<span>模型: yolov8n, yolov8s</span>
</div>
</div>
<!-- FPS显示 -->
<div class="fps-display">
<span>FPS: <strong>24.5</strong></span>
</div>
</div>
<!-- 播放控制 -->
<div class="video-controls" id="videoControls" style="display: none;">
<div class="controls-bg"></div>
<div class="controls-content">
<div class="d-flex justify-content-between align-items-center">
<div>
<button class="btn btn-sm btn-light me-2" id="playPauseBtn">
<i class="bi bi-pause"></i>
</button>
<button class="btn btn-sm btn-light" id="volumeBtn">
<i class="bi bi-volume-up"></i>
</button>
</div>
<div class="flex-grow-1 mx-3">
<div class="progress" style="height: 4px;">
<div class="progress-bar" style="width: 45%"></div>
</div>
</div>
<div>
<span class="text-light me-3">12:45 / 30:00</span>
<button class="btn btn-sm btn-light" id="settingsBtn">
<i class="bi bi-gear"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer bg-white">
<div class="row">
<div class="col-md-6">
<h6>检测统计</h6>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-primary">人: 3</span>
<span class="badge bg-success">汽车: 2</span>
<span class="badge bg-warning">自行车: 1</span>
<span class="badge bg-danger">卡车: 1</span>
</div>
</div>
<div class="col-md-6 text-md-end">
<h6>流信息</h6>
<p class="mb-0 small">
<span class="text-muted">URL:</span>
<code id="currentStreamUrl">rtmp://localhost:1935/live/camera1</code>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4 mb-4">
<!-- 流列表 -->
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">可用视频流</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush" id="streamList">
<!-- 流列表项将通过JavaScript动态生成 -->
</div>
</div>
</div>
<!-- 检测详情 -->
<div class="card border-0 shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0">实时检测详情</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm" style="height:300px">
<thead>
<tr>
<th>类型</th>
<th>置信度</th>
<th>位置</th>
<th>模型</th>
</tr>
</thead>
<tbody id="detectionDetails">
<!-- 检测详情将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<div class="text-center mt-3">
<div class="spinner-border spinner-border-sm text-primary" role="status" id="detectionSpinner">
<span class="visually-hidden">检测中...</span>
</div>
<small class="text-muted ms-2">实时检测中...</small>
</div>
</div>
</div>
</div>
</div>
<!-- 历史记录 -->
<div class="card border-0 shadow-sm mt-4 col-lg-8">
<div class="card-header bg-white">
<h5 class="mb-0">视频录制历史</h5>
</div>
<div class="card-body">
<div class="row" id="recordingHistory">
<!-- 录制历史将通过JavaScript动态生成 -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 添加视频流模态框 -->
<div class="modal fade" id="addStreamModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加视频流</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addStreamForm">
<div class="mb-3">
<label for="streamName" class="form-label">流名称</label>
<input type="text" class="form-control" id="streamName" placeholder="例如:交通监控-路口A" required>
</div>
<div class="mb-3">
<label for="streamUrl" class="form-label">流地址</label>
<input type="text" class="form-control" id="streamUrl" placeholder="例如rtmp://example.com/live/stream" required>
</div>
<div class="mb-3">
<label for="streamType" class="form-label">流类型</label>
<select class="form-select" id="streamType">
<option value="rtmp">RTMP</option>
<option value="rtsp">RTSP</option>
<option value="http-flv">HTTP-FLV</option>
<option value="hls">HLS</option>
<option value="webrtc">WebRTC</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="enableDetection" checked>
<label class="form-check-label" for="enableDetection">
启用AI检测
</label>
</div>
</div>
<div class="mb-3" id="modelSelection" style="display: none;">
<label class="form-label">选择检测模型</label>
<div class="border rounded p-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="detectPerson" checked>
<label class="form-check-label" for="detectPerson">
行人检测
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="detectVehicle" checked>
<label class="form-check-label" for="detectVehicle">
车辆检测
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="detectBicycle">
<label class="form-check-label" for="detectBicycle">
自行车检测
</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="submitStreamForm">添加流</button>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white mt-5 py-4">
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<h5>视频播放</h5>
<p class="text-light">多任务YOLO检测系统的视频播放界面</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0">© 2023 多任务YOLO检测系统 | 演示版本 v1.0.0</p>
<p class="text-light small">此页面为演示版本,使用模拟数据</p>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 模拟视频流数据
const mockStreams = [
{
id: 1,
name: "交通监控-路口A",
url: "rtmp://localhost:1935/live/camera1",
type: "rtmp",
status: "online",
fps: 24.5,
resolution: "1920x1080",
detectionEnabled: true,
models: ["yolov8n.pt", "yolov8s.pt"],
lastActive: "2分钟前"
},
{
id: 2,
name: "安全监控-入口",
url: "rtmp://localhost:1935/live/camera2",
type: "rtmp",
status: "online",
fps: 28.1,
resolution: "1280x720",
detectionEnabled: true,
models: ["yolov8n.pt"],
lastActive: "5分钟前"
},
{
id: 3,
name: "停车场监控",
url: "rtsp://192.168.1.100:554/live",
type: "rtsp",
status: "offline",
fps: 0,
resolution: "1920x1080",
detectionEnabled: false,
models: [],
lastActive: "1小时前"
},
{
id: 4,
name: "生产线检测",
url: "http://192.168.1.101:8000/live.flv",
type: "http-flv",
status: "online",
fps: 15.3,
resolution: "1280x720",
detectionEnabled: true,
models: ["custom.pt"],
lastActive: "10分钟前"
},
{
id: 5,
name: "测试流",
url: "rtmp://test.com:1935/live/test",
type: "rtmp",
status: "online",
fps: 30.0,
resolution: "1920x1080",
detectionEnabled: false,
models: [],
lastActive: "刚刚"
}
];
// 模拟检测数据
const mockDetections = [
{ type: "人", confidence: 0.92, position: "x:320, y:240", model: "yolov8n.pt" },
{ type: "汽车", confidence: 0.87, position: "x:480, y:280", model: "yolov8s.pt" },
{ type: "自行车", confidence: 0.78, position: "x:160, y:360", model: "yolov8n.pt" },
{ type: "卡车", confidence: 0.81, position: "x:560, y:320", model: "yolov8s.pt" },
{ type: "人", confidence: 0.88, position: "x:400, y:200", model: "yolov8n.pt" }
];
// 模拟录制历史
const mockRecordings = [
{ id: 1, name: "交通监控-20231015-0930.mp4", duration: "5分30秒", size: "125MB", date: "2023-10-15 09:30", stream: "交通监控-路口A" },
{ id: 2, name: "安全监控-20231015-1015.mp4", duration: "3分15秒", size: "78MB", date: "2023-10-15 10:15", stream: "安全监控-入口" },
{ id: 3, name: "生产线-20231015-0845.mp4", duration: "10分45秒", size: "245MB", date: "2023-10-15 08:45", stream: "生产线检测" },
];
// 当前选中的流
let currentStream = null;
let isPlaying = false;
let detectionInterval = null;
// 渲染流列表
function renderStreamList() {
const list = document.getElementById('streamList');
list.innerHTML = '';
mockStreams.forEach(stream => {
const item = document.createElement('a');
item.href = '#';
item.className = 'list-group-item list-group-item-action';
if (currentStream && currentStream.id === stream.id) {
item.classList.add('active');
}
const statusColor = stream.status === 'online' ? 'success' : 'secondary';
const statusText = stream.status === 'online' ? '在线' : '离线';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${stream.name}</h6>
<p class="mb-1 small text-muted">
<code>${stream.url}</code>
</p>
<small>
<span class="badge bg-${statusColor} me-2">${statusText}</span>
${stream.detectionEnabled ? '<span class="badge bg-primary me-2">AI检测</span>' : ''}
<span class="text-muted">${stream.resolution}</span>
</small>
</div>
<div class="text-end">
<div class="mb-1">
<span class="badge bg-info">${stream.fps > 0 ? stream.fps.toFixed(1) + ' FPS' : '离线'}</span>
</div>
<small class="text-muted">${stream.lastActive}</small>
</div>
</div>
`;
item.addEventListener('click', function(e) {
e.preventDefault();
selectStream(stream);
});
list.appendChild(item);
});
}
// 渲染检测详情
function renderDetectionDetails() {
const tbody = document.getElementById('detectionDetails');
tbody.innerHTML = '';
mockDetections.forEach(detection => {
const row = document.createElement('tr');
// 置信度颜色
let confidenceColor = 'success';
if (detection.confidence < 0.7) {
confidenceColor = 'warning';
} else if (detection.confidence < 0.5) {
confidenceColor = 'danger';
}
row.innerHTML = `
<td>
<span class="badge bg-primary">${detection.type}</span>
</td>
<td>
<span class="badge bg-${confidenceColor}">${(detection.confidence * 100).toFixed(1)}%</span>
</td>
<td><small>${detection.position}</small></td>
<td><small>${detection.model}</small></td>
`;
tbody.appendChild(row);
});
}
// 渲染录制历史
function renderRecordingHistory() {
const container = document.getElementById('recordingHistory');
container.innerHTML = '';
mockRecordings.forEach(recording => {
const col = document.createElement('div');
col.className = 'col-md-3 mb-3';
col.innerHTML = `
<div class="card h-100">
<div class="card-body">
<h6 class="card-title small">${recording.name}</h6>
<p class="card-text small text-muted">
<i class="bi bi-clock me-1"></i>${recording.duration}<br>
<i class="bi bi-hdd me-1"></i>${recording.size}<br>
<i class="bi bi-camera-video me-1"></i>${recording.stream}
</p>
</div>
<div class="card-footer bg-white">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">${recording.date}</small>
<div>
<button class="btn btn-sm btn-outline-primary me-1">
<i class="bi bi-play"></i>
</button>
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-download"></i>
</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(col);
});
}
// 选择视频流
function selectStream(stream) {
currentStream = stream;
// 更新UI
document.getElementById('currentStreamTitle').textContent = stream.name;
document.getElementById('currentStreamUrl').textContent = stream.url;
// 显示模拟视频
document.getElementById('videoPlaceholder').style.display = 'none';
document.getElementById('simulatedVideo').style.display = 'block';
document.getElementById('videoControls').style.display = 'block';
// 开始模拟播放
if (!isPlaying) {
togglePlayPause();
}
// 更新流列表活动状态
renderStreamList();
// 开始模拟检测更新
startDetectionSimulation();
// 显示通知
showNotification('视频流已切换', `已切换到 ${stream.name}`, 'success');
}
// 开始模拟检测
function startDetectionSimulation() {
// 清除之前的定时器
if (detectionInterval) {
clearInterval(detectionInterval);
}
// 显示加载动画
const spinner = document.getElementById('detectionSpinner');
spinner.style.display = 'inline-block';
// 模拟检测更新
detectionInterval = setInterval(() => {
// 随机更新检测数据
mockDetections.forEach(detection => {
// 随机微调置信度
const change = (Math.random() - 0.5) * 0.1;
detection.confidence = Math.max(0.3, Math.min(0.99, detection.confidence + change));
// 随机微调位置
if (Math.random() > 0.7) {
const x = Math.floor(Math.random() * 640);
const y = Math.floor(Math.random() * 480);
detection.position = `x:${x}, y:${y}`;
}
});
// 随机添加或删除检测
if (Math.random() > 0.8 && mockDetections.length < 8) {
// 添加新检测
const types = ["人", "汽车", "自行车", "卡车", "摩托车", "公交车"];
const models = ["yolov8n.pt", "yolov8s.pt", "yolov8m.pt"];
const newType = types[Math.floor(Math.random() * types.length)];
const newModel = models[Math.floor(Math.random() * models.length)];
mockDetections.push({
type: newType,
confidence: Math.random() * 0.3 + 0.6,
position: `x:${Math.floor(Math.random() * 640)}, y:${Math.floor(Math.random() * 480)}`,
model: newModel
});
} else if (Math.random() > 0.9 && mockDetections.length > 3) {
// 移除检测
mockDetections.splice(Math.floor(Math.random() * mockDetections.length), 1);
}
// 更新检测详情表格
renderDetectionDetails();
// 更新检测统计
updateDetectionStats();
}, 2000);
}
// 更新检测统计
function updateDetectionStats() {
// 统计各类检测数量
const counts = {};
mockDetections.forEach(detection => {
counts[detection.type] = (counts[detection.type] || 0) + 1;
});
// 这里可以更新检测统计的显示
// 在实际实现中,可以更新页面上的统计信息
}
// 切换播放/暂停
function togglePlayPause() {
const button = document.getElementById('playPauseBtn');
const icon = button.querySelector('i');
if (isPlaying) {
// 暂停
icon.className = 'bi bi-play';
button.setAttribute('title', '播放');
isPlaying = false;
// 暂停检测更新
if (detectionInterval) {
clearInterval(detectionInterval);
detectionInterval = null;
}
// 隐藏加载动画
document.getElementById('detectionSpinner').style.display = 'none';
} else {
// 播放
icon.className = 'bi bi-pause';
button.setAttribute('title', '暂停');
isPlaying = true;
// 开始检测更新
startDetectionSimulation();
}
}
// 切换全屏
function toggleFullscreen() {
const container = document.getElementById('videoContainer');
if (!document.fullscreenElement) {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
container.msRequestFullscreen();
}
document.getElementById('fullscreenBtn').innerHTML = '<i class="bi bi-fullscreen-exit"></i>';
showNotification('全屏模式', '已进入全屏模式', 'info');
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
document.getElementById('fullscreenBtn').innerHTML = '<i class="bi bi-arrows-fullscreen"></i>';
showNotification('退出全屏', '已退出全屏模式', 'info');
}
}
// 截图
function takeScreenshot() {
// 模拟截图
showNotification('截图已保存', '视频截图已保存到下载文件夹', 'success');
// 在实际实现中,这里可以调用截图功能
}
// 添加新视频流
function addNewStream() {
const name = document.getElementById('streamName').value;
const url = document.getElementById('streamUrl').value;
const type = document.getElementById('streamType').value;
const enableDetection = document.getElementById('enableDetection').checked;
if (!name || !url) {
showNotification('表单验证失败', '请填写流名称和地址', 'danger');
return;
}
// 创建新流
const newStream = {
id: mockStreams.length + 1,
name: name,
url: url,
type: type,
status: "online",
fps: Math.floor(Math.random() * 10) + 20,
resolution: "1920x1080",
detectionEnabled: enableDetection,
models: enableDetection ? ["yolov8n.pt"] : [],
lastActive: "刚刚"
};
mockStreams.push(newStream);
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('addStreamModal')).hide();
// 重置表单
document.getElementById('addStreamForm').reset();
// 更新流列表
renderStreamList();
// 选择新添加的流
selectStream(newStream);
showNotification('流添加成功', `已添加视频流: ${name}`, 'success');
}
// 刷新流列表
function refreshStreams() {
// 模拟更新流状态
mockStreams.forEach(stream => {
// 随机改变一些流的状态
if (Math.random() > 0.7) {
stream.status = stream.status === 'online' ? 'offline' : 'online';
stream.lastActive = stream.status === 'online' ? '刚刚' : '1分钟前';
}
// 更新FPS
if (stream.status === 'online') {
stream.fps = (Math.random() * 10 + 20).toFixed(1);
} else {
stream.fps = 0;
}
});
// 重新渲染流列表
renderStreamList();
showNotification('流列表已刷新', '视频流状态已更新', 'info');
}
// 显示通知
function showNotification(title, message, type) {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
notification.innerHTML = `
<strong>${title}</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 3000);
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 渲染初始数据
renderStreamList();
renderDetectionDetails();
renderRecordingHistory();
// 默认选择第一个流
if (mockStreams.length > 0) {
selectStream(mockStreams[0]);
}
// 绑定事件
document.getElementById('playPauseBtn').addEventListener('click', togglePlayPause);
document.getElementById('fullscreenBtn').addEventListener('click', toggleFullscreen);
document.getElementById('screenshotBtn').addEventListener('click', takeScreenshot);
document.getElementById('submitStreamForm').addEventListener('click', addNewStream);
document.getElementById('refreshStreams').addEventListener('click', refreshStreams);
// 检测启用复选框事件
document.getElementById('enableDetection').addEventListener('change', function() {
const modelSelection = document.getElementById('modelSelection');
modelSelection.style.display = this.checked ? 'block' : 'none';
});
// 显示欢迎消息
setTimeout(() => {
showNotification('视频播放器已就绪', '选择左侧的视频流开始播放', 'success');
}, 1000);
});
</script>
</body>
</html>

View File

@ -0,0 +1,841 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能监控视频系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #0d1117;
color: #e6edf3;
width: 1920px;
height: 1080px;
overflow: hidden;
display: flex;
}
/* 侧边栏样式 */
.sidebar {
width: 320px;
background-color: #161b22;
border-right: 1px solid #30363d;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
padding: 24px 20px;
border-bottom: 1px solid #30363d;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.logo i {
font-size: 28px;
color: #58a6ff;
}
.logo h1 {
font-size: 22px;
font-weight: 600;
}
.status-info {
display: flex;
justify-content: space-between;
margin-top: 16px;
font-size: 14px;
}
.online {
color: #3fb950;
}
.offline {
color: #f85149;
}
.camera-list {
flex: 1;
overflow-y: auto;
padding: 16px 0;
}
.camera-list::-webkit-scrollbar {
width: 6px;
}
.camera-list::-webkit-scrollbar-thumb {
background-color: #30363d;
border-radius: 3px;
}
.camera-item {
padding: 16px 20px;
border-bottom: 1px solid #21262d;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 12px;
}
.camera-item:hover {
background-color: #1c2128;
}
.camera-item.active {
background-color: #1c2128;
border-left: 3px solid #58a6ff;
}
.camera-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background-color: #238636;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.camera-info {
flex: 1;
}
.camera-name {
font-weight: 600;
margin-bottom: 4px;
}
.camera-location {
font-size: 13px;
color: #8b949e;
}
.camera-status {
width: 10px;
height: 10px;
border-radius: 50%;
}
.camera-status.online {
background-color: #3fb950;
}
.camera-status.offline {
background-color: #f85149;
}
.camera-status.recording {
background-color: #f0883e;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
/* 主内容区样式 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
overflow: hidden;
}
.controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #30363d;
}
.layout-controls {
display: flex;
gap: 12px;
}
.layout-btn {
padding: 10px 20px;
background-color: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
font-weight: 500;
}
.layout-btn:hover {
background-color: #30363d;
border-color: #8b949e;
}
.layout-btn.active {
background-color: #1f6feb;
border-color: #1f6feb;
color: white;
}
.time-display {
font-size: 16px;
color: #8b949e;
display: flex;
align-items: center;
gap: 8px;
}
/* 视频网格样式 */
.video-grid {
flex: 1;
display: grid;
gap: 16px;
transition: all 0.3s;
}
.video-grid.single {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.video-grid.four {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.video-grid.nine {
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
}
.video-player {
background-color: #000;
border-radius: 8px;
overflow: hidden;
position: relative;
border: 1px solid #30363d;
display: flex;
flex-direction: column;
}
.video-header {
padding: 12px 16px;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #30363d;
}
.video-title {
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.video-controls {
display: flex;
gap: 12px;
}
.control-btn {
background: none;
border: none;
color: #c9d1d9;
cursor: pointer;
font-size: 14px;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.control-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.video-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.video-feed {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-video {
color: #8b949e;
font-size: 16px;
text-align: center;
}
.playing-indicator {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: #f85149;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
}
/* 时间更新函数 */
.time-update {
margin-top: 20px;
text-align: center;
color: #8b949e;
font-size: 14px;
}
/* 响应式调整 */
@media (max-width: 1920px) {
body {
width: 100%;
height: 100vh;
}
}
</style>
</head>
<body>
<!-- 左侧边栏 -->
<div class="sidebar">
<div class="header">
<div class="logo">
<i class="fas fa-shield-alt"></i>
<h1>智能监控系统</h1>
</div>
<p>监控中心 v2.1</p>
<div class="status-info">
<div class="online">
<i class="fas fa-circle"></i> 在线: <span id="online-count">8</span>
</div>
<div class="offline">
<i class="fas fa-circle"></i> 离线: <span id="offline-count">2</span>
</div>
</div>
</div>
<div class="camera-list" id="camera-list">
<!-- 摄像头列表将通过JavaScript动态生成 -->
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="controls">
<div class="layout-controls">
<button class="layout-btn active" data-layout="single">
<i class="fas fa-expand"></i> 单画面
</button>
<button class="layout-btn" data-layout="four">
<i class="fas fa-th-large"></i> 四画面
</button>
<button class="layout-btn" data-layout="nine">
<i class="fas fa-th"></i> 九画面
</button>
</div>
<div class="time-display">
<i class="far fa-clock"></i>
<span id="current-time">--:--:--</span>
</div>
</div>
<div class="video-grid nine" id="video-grid">
<!-- 视频播放器将通过JavaScript动态生成 -->
</div>
<div class="time-update">
<p>最后更新: <span id="last-update">--:--:--</span> | 系统运行正常</p>
</div>
</div>
<script>
// 模拟摄像头数据
const cameras = [
{ id: 1, name: "主入口", location: "一楼大厅", status: "online", recording: true, feed: "https://assets.mixkit.co/videos/preview/mixkit-security-camera-on-the-corner-of-a-building-42939-large.mp4" },
{ id: 2, name: "停车场", location: "地下B1层", status: "online", recording: true, feed: "https://assets.mixkit.co/videos/preview/mixkit-parking-lot-entrance-from-a-high-angle-42942-large.mp4" },
{ id: 3, name: "电梯间", location: "1号电梯", status: "online", recording: false, feed: "https://assets.mixkit.co/videos/preview/mixkit-going-up-the-stairs-of-a-modern-building-42944-large.mp4" },
{ id: 4, name: "财务室", location: "三楼301", status: "online", recording: true, feed: "https://assets.mixkit.co/videos/preview/mixkit-woman-working-on-a-laptop-42945-large.mp4" },
{ id: 5, name: "仓库", location: "地下一层", status: "offline", recording: false, feed: "" },
{ id: 6, name: "侧门", location: "建筑东侧", status: "online", recording: false, feed: "https://assets.mixkit.co/videos/preview/mixkit-exit-sign-on-a-building-wall-42943-large.mp4" },
{ id: 7, name: "办公室", location: "二楼201", status: "online", recording: false, feed: "https://assets.mixkit.co/videos/preview/mixkit-open-plan-office-with-modern-design-42941-large.mp4" },
{ id: 8, name: "走廊", location: "三楼北侧", status: "online", recording: true, feed: "https://assets.mixkit.co/videos/preview/mixkit-hallway-in-a-modern-office-building-42940-large.mp4" },
{ id: 9, name: "后院", location: "建筑后方", status: "offline", recording: false, feed: "" },
{ id: 10, name: "接待处", location: "一楼前台", status: "online", recording: true, feed: "https://assets.mixkit.co/videos/preview/mixkit-empty-reception-of-a-modern-hotel-42938-large.mp4" }
];
// 当前选中的摄像头
let selectedCameraId = 1;
// 当前布局模式
let currentLayout = 'nine';
// 视频播放器数组
let videoPlayers = [];
// DOM 加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initCameraList();
initVideoGrid();
initLayoutControls();
updateTime();
setInterval(updateTime, 1000);
// 模拟视频播放
simulateVideoPlayback();
});
// 初始化摄像头列表
function initCameraList() {
const cameraList = document.getElementById('camera-list');
cameras.forEach(camera => {
const cameraItem = document.createElement('div');
cameraItem.className = `camera-item ${camera.id === selectedCameraId ? 'active' : ''}`;
cameraItem.dataset.id = camera.id;
// 根据状态设置图标颜色
let iconColor = '#238636'; // 默认绿色
let iconClass = 'fas fa-video';
if (camera.status === 'offline') {
iconColor = '#6e7681';
iconClass = 'fas fa-video-slash';
}
cameraItem.innerHTML = `
<div class="camera-icon" style="background-color: ${iconColor};">
<i class="${iconClass}"></i>
</div>
<div class="camera-info">
<div class="camera-name">${camera.name}</div>
<div class="camera-location">${camera.location}</div>
</div>
<div class="camera-status ${camera.status} ${camera.recording ? 'recording' : ''}"></div>
`;
cameraItem.addEventListener('click', function() {
// 移除所有active类
document.querySelectorAll('.camera-item').forEach(item => {
item.classList.remove('active');
});
// 添加active类到当前项
this.classList.add('active');
// 更新选中的摄像头
selectedCameraId = parseInt(this.dataset.id);
// 在第一个可用的视频播放器中播放选中的摄像头
playCameraInFirstAvailable(selectedCameraId);
});
cameraList.appendChild(cameraItem);
});
// 更新在线/离线计数
updateCameraCount();
}
// 初始化视频网格
function initVideoGrid() {
const videoGrid = document.getElementById('video-grid');
// 创建9个视频播放器
for (let i = 0; i < 9; i++) {
const cameraIndex = i < cameras.length ? i : 0;
const camera = cameras[cameraIndex];
const videoPlayer = document.createElement('div');
videoPlayer.className = 'video-player';
videoPlayer.dataset.index = i;
videoPlayer.innerHTML = `
<div class="video-header">
<div class="video-title">
<i class="fas fa-video"></i>
<span class="title-text">${camera.name}</span>
</div>
<div class="video-controls">
<button class="control-btn" title="全屏">
<i class="fas fa-expand"></i>
</button>
<button class="control-btn" title="声音">
<i class="fas fa-volume-up"></i>
</button>
<button class="control-btn" title="设置">
<i class="fas fa-cog"></i>
</button>
</div>
</div>
<div class="video-container">
${camera.status === 'online' ?
`<video class="video-feed" muted loop>
<source src="${camera.feed}" type="video/mp4">
您的浏览器不支持视频标签。
</video>` :
`<div class="no-video">
<i class="fas fa-wifi-slash"></i>
<p>摄像头离线</p>
</div>`
}
${camera.recording ?
`<div class="playing-indicator">
<i class="fas fa-circle"></i> 录制中
</div>` : ''
}
</div>
`;
videoGrid.appendChild(videoPlayer);
// 保存视频元素引用
if (camera.status === 'online') {
const videoElement = videoPlayer.querySelector('video');
if (videoElement) {
videoPlayers.push(videoElement);
}
}
// 添加点击事件,点击视频播放器时选中对应的摄像头
videoPlayer.addEventListener('click', function(e) {
if (!e.target.closest('.video-controls')) {
const cameraId = cameras[cameraIndex].id;
selectedCameraId = cameraId;
// 更新摄像头列表选中状态
document.querySelectorAll('.camera-item').forEach(item => {
item.classList.remove('active');
if (parseInt(item.dataset.id) === cameraId) {
item.classList.add('active');
}
});
}
});
// 全屏按钮事件
const fullscreenBtn = videoPlayer.querySelector('.control-btn');
fullscreenBtn.addEventListener('click', function() {
const videoContainer = videoPlayer.querySelector('.video-container');
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
}
});
}
}
// 初始化布局控制
function initLayoutControls() {
const layoutBtns = document.querySelectorAll('.layout-btn');
layoutBtns.forEach(btn => {
btn.addEventListener('click', function() {
// 移除所有active类
layoutBtns.forEach(b => b.classList.remove('active'));
// 添加active类到当前按钮
this.classList.add('active');
// 更新布局
const layout = this.dataset.layout;
changeLayout(layout);
});
});
}
// 更改布局
function changeLayout(layout) {
const videoGrid = document.getElementById('video-grid');
currentLayout = layout;
// 更新网格类
videoGrid.className = 'video-grid ' + layout;
// 根据布局显示/隐藏视频播放器
const videoPlayers = document.querySelectorAll('.video-player');
if (layout === 'single') {
// 单画面:只显示第一个,其他隐藏
videoPlayers.forEach((player, index) => {
player.style.display = index === 0 ? 'flex' : 'none';
});
} else if (layout === 'four') {
// 四画面显示前4个其他隐藏
videoPlayers.forEach((player, index) => {
player.style.display = index < 4 ? 'flex' : 'none';
});
} else {
// 九画面:显示所有
videoPlayers.forEach(player => {
player.style.display = 'flex';
});
}
}
// 在第一个可用的视频播放器中播放选中的摄像头
function playCameraInFirstAvailable(cameraId) {
const camera = cameras.find(c => c.id === cameraId);
if (!camera || camera.status === 'offline') return;
// 根据布局找到第一个可用的视频播放器
let targetIndex = 0;
if (currentLayout === 'single') {
targetIndex = 0;
} else if (currentLayout === 'four') {
// 在四宫格中寻找第一个不是正在录制重要视频的播放器
for (let i = 0; i < 4; i++) {
const currentCamera = cameras[i];
if (!currentCamera.recording) {
targetIndex = i;
break;
}
}
} else {
// 在九宫格中寻找第一个不是正在录制重要视频的播放器
for (let i = 0; i < 9; i++) {
const currentCamera = cameras[i];
if (!currentCamera.recording) {
targetIndex = i;
break;
}
}
}
// 更新目标视频播放器
const videoPlayer = document.querySelectorAll('.video-player')[targetIndex];
if (!videoPlayer) return;
// 更新标题
const titleText = videoPlayer.querySelector('.title-text');
titleText.textContent = camera.name;
// 更新视频源
const videoContainer = videoPlayer.querySelector('.video-container');
videoContainer.innerHTML = `
<video class="video-feed" muted autoplay loop>
<source src="${camera.feed}" type="video/mp4">
您的浏览器不支持视频标签。
</video>
${camera.recording ?
`<div class="playing-indicator">
<i class="fas fa-circle"></i> 录制中
</div>` : ''
}
`;
// 更新录制指示器
const recordingIndicator = videoPlayer.querySelector('.playing-indicator');
if (camera.recording && !recordingIndicator) {
const indicator = document.createElement('div');
indicator.className = 'playing-indicator';
indicator.innerHTML = '<i class="fas fa-circle"></i> 录制中';
videoContainer.appendChild(indicator);
}
// 播放视频
const videoElement = videoContainer.querySelector('video');
if (videoElement) {
videoElement.play().catch(e => console.log("自动播放被阻止:", e));
// 更新视频播放器数组
if (!videoPlayers.includes(videoElement)) {
videoPlayers.push(videoElement);
}
}
}
// 更新在线/离线摄像头计数
function updateCameraCount() {
const onlineCount = cameras.filter(c => c.status === 'online').length;
const offlineCount = cameras.length - onlineCount;
document.getElementById('online-count').textContent = onlineCount;
document.getElementById('offline-count').textContent = offlineCount;
}
// 更新时间显示
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN');
const dateString = now.toLocaleDateString('zh-CN');
document.getElementById('current-time').textContent = timeString;
document.getElementById('last-update').textContent = `${dateString} ${timeString}`;
}
// 模拟视频播放
function simulateVideoPlayback() {
// 播放所有在线的视频
videoPlayers.forEach(video => {
video.play().catch(e => console.log("视频播放失败:", e));
});
// 随机切换一些摄像头的状态(模拟实时变化)
setInterval(() => {
// 随机选择一个摄像头切换在线状态10%概率)
if (Math.random() < 0.1) {
const randomIndex = Math.floor(Math.random() * cameras.length);
const camera = cameras[randomIndex];
// 切换状态
if (camera.status === 'online') {
camera.status = 'offline';
} else {
camera.status = 'online';
}
// 更新UI
updateCameraUI(randomIndex);
updateCameraCount();
}
// 随机开始/停止录制5%概率)
if (Math.random() < 0.05) {
const randomIndex = Math.floor(Math.random() * cameras.length);
const camera = cameras[randomIndex];
if (camera.status === 'online') {
camera.recording = !camera.recording;
updateRecordingUI(randomIndex);
}
}
}, 5000);
}
// 更新摄像头UI状态
function updateCameraUI(index) {
const camera = cameras[index];
const cameraItem = document.querySelector(`.camera-item[data-id="${camera.id}"]`);
if (!cameraItem) return;
// 更新状态指示灯
const statusIndicator = cameraItem.querySelector('.camera-status');
statusIndicator.className = 'camera-status ' + camera.status;
// 更新图标
const cameraIcon = cameraItem.querySelector('.camera-icon i');
if (camera.status === 'offline') {
cameraIcon.className = 'fas fa-video-slash';
cameraItem.querySelector('.camera-icon').style.backgroundColor = '#6e7681';
} else {
cameraIcon.className = 'fas fa-video';
cameraItem.querySelector('.camera-icon').style.backgroundColor = '#238636';
}
// 更新对应的视频播放器
const videoPlayer = document.querySelectorAll('.video-player')[index];
if (videoPlayer) {
const videoContainer = videoPlayer.querySelector('.video-container');
if (camera.status === 'online') {
videoContainer.innerHTML = `
<video class="video-feed" muted autoplay loop>
<source src="${camera.feed}" type="video/mp4">
您的浏览器不支持视频标签。
</video>
${camera.recording ?
`<div class="playing-indicator">
<i class="fas fa-circle"></i> 录制中
</div>` : ''
}
`;
// 播放视频
const videoElement = videoContainer.querySelector('video');
if (videoElement) {
videoElement.play().catch(e => console.log("视频播放失败:", e));
// 添加到视频播放器数组
if (!videoPlayers.includes(videoElement)) {
videoPlayers.push(videoElement);
}
}
} else {
videoContainer.innerHTML = `
<div class="no-video">
<i class="fas fa-wifi-slash"></i>
<p>摄像头离线</p>
</div>
`;
}
}
}
// 更新录制UI状态
function updateRecordingUI(index) {
const camera = cameras[index];
const videoPlayer = document.querySelectorAll('.video-player')[index];
if (!videoPlayer) return;
// 更新录制指示器
const videoContainer = videoPlayer.querySelector('.video-container');
let recordingIndicator = videoContainer.querySelector('.playing-indicator');
if (camera.recording) {
if (!recordingIndicator) {
recordingIndicator = document.createElement('div');
recordingIndicator.className = 'playing-indicator';
recordingIndicator.innerHTML = '<i class="fas fa-circle"></i> 录制中';
videoContainer.appendChild(recordingIndicator);
}
// 更新摄像头列表中的状态指示灯
const cameraItem = document.querySelector(`.camera-item[data-id="${camera.id}"]`);
if (cameraItem) {
const statusIndicator = cameraItem.querySelector('.camera-status');
statusIndicator.classList.add('recording');
}
} else {
if (recordingIndicator) {
recordingIndicator.remove();
}
// 更新摄像头列表中的状态指示灯
const cameraItem = document.querySelector(`.camera-item[data-id="${camera.id}"]`);
if (cameraItem) {
const statusIndicator = cameraItem.querySelector('.camera-status');
statusIndicator.classList.remove('recording');
}
}
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -9,8 +9,8 @@ from log import logger
if __name__ == "__main__":
model_config = {
'model_path': r"F:\PyModelScope\Yolov\models\car1.pt",
'imgs': r"E:\DC\Yolov\smart_tiles\smart_tile_002_004_1325x1326.png"
'model_path': r"F:\PyModelScope\Yolov\models\best-fbrt.pt",
'imgs': r"E:\DC\Yolov\uploads\费县8-13-19-9-14-20-14.JPG"
}
# encryption_key = "O3^doTYEpyT%VCYdI6u#YKKi7YFWjGZQ"
@ -34,7 +34,7 @@ if __name__ == "__main__":
source=model_config['imgs'],
stream=False,
verbose=False,
conf=0.2,
conf=0.5,
iou=0.45,
imgsz=1920,
)

Binary file not shown.

11
xiaomubiao.py Normal file
View File

@ -0,0 +1,11 @@
from sahi import AutoDetectionModel
from sahi.predict import get_prediction, get_sliced_prediction
ckpt_path = "yolov8l.pt"
detection_model = AutoDetectionModel.from_pretrained(
model_type='yolov8',
model_path=ckpt_path,
confidence_threshold=0.25, ## same as the default value for our base model
image_size=640,
device="cpu", # or 'cuda' if you have access to GPU
)

File diff suppressed because one or more lines are too long