更新多个算法问题
|
|
@ -15,3 +15,5 @@ yolo_detection.log
|
|||
*.pyc
|
||||
encrypted_models
|
||||
temp_uploads
|
||||
*.pt
|
||||
video
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" # 模型运行的设备 cuda:0表示第一个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
|
||||
1
main.py
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 5.8 MiB |
|
Before Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 5.4 MiB |
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
6
test.py
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
BIN
test_video.mp4
|
|
@ -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
|
||||
)
|
||||