Compare commits

...

16 Commits

Author SHA1 Message Date
砂糖
4f064c2e3e feat: 增加设备名称 2025-10-09 10:00:09 +08:00
98c2aeaa9b Merge remote-tracking branch 'origin/master' 2025-10-09 09:54:53 +08:00
98042b237c feat(video): 添加设备名称字段并优化巡检任务关联查询
- 在 Device 实体类中新增 deviceName 字段及其 getter/setter 方法
- 更新 DeviceMapper.xml,支持 device_name 字段的查询、插入和更新操作
- 修改 InspectionTask 实体类,增加 deviceIp 字段用于联查返回设备 IP
- 调整 InspectionTaskMapper.xml,通过左连接获取设备名称与 IP 信息- 移除冗余的 device_name 插入与更新逻辑
- 注释掉旧有的设备信息设置代码,避免重复赋值
- 更新 toString 方法以包含新的 deviceName 属性
2025-10-09 09:54:40 +08:00
砂糖
bf0996d750 修改首页设备列表 2025-10-08 16:16:41 +08:00
砂糖
3abac5ff1b feat: 报警页面完善 2025-10-08 13:53:54 +08:00
99a8a943bc feat(video): 新增报警批量处理功能并优化任务执行逻辑
- 新增 alarmBatchBo 类用于批量处理报警记录
- 移除报警记录控制器中的权限注解
- 批量处理接口改为接收 alarmBatchBo 对象
- 引入 ruoyi-quartz 依赖用于定时任务处理- 恢复并优化 InspectionTaskServiceImpl 中设备信息设置
- 更新任务执行时更新最后执行时间与下次执行时间- 视频分析服务中增加报警记录时更新任务报警次数
2025-10-08 13:40:04 +08:00
f0b4c5a8bf 提高视频帧率 2025-10-08 11:51:28 +08:00
砂糖
f40d6ffcb6 1 2025-10-08 10:09:48 +08:00
砂糖
524c8343e6 feat: 增加打印信息 2025-10-08 10:00:36 +08:00
aa32f9e9ac fix(models): 解决 PyTorch 2.6+ 兼容性问题
- 在 garbage_model.py 和 smoke_model.py 中添加 weights_only=False 参数以允许加载模型类结构
- 修复 HTTP YOLO 检测器中的文件上传和响应解析逻辑- 移除不必要的导入并优化代码结构
- 添加自定义字节数组资源类以支持 RestTemplate 文件上传- 改进错误处理和日志记录机制
2025-10-07 17:53:34 +08:00
5f6058c024 refactor(detector):重构HTTP YOLO检测器实现
- 使用ByteArrayResource替代自定义资源类
- 将model_name参数移至URL查询参数
-优化响应解析逻辑,增强类型检查
- 改进错误处理和空值判断
- 清理无用的导入和代码格式化- 修复潜在的编码异常处理问题
2025-10-07 16:57:03 +08:00
e3701991ef fix(video): 修改Python服务URL为本地地址
- 将检测器API地址从容器名改为localhost- 更新PYTHON_API_URL常量值为http://localhost:8000/api/detect/file
2025-10-07 16:35:26 +08:00
7096359434 feat(video): 添加模型名称字段以支持动态模型选择
- 在 InspectionTask 实体类中新增 modelName 字段及其 getter/setter 方法
- 更新 MyBatis 映射文件,增加对 model_name 字段的映射和支持
- 修改 SQL 查询语句,在查询条件和插入、更新操作中加入 modelName 字段处理
- 调整 VideoAnalysisService 中的模型选择逻辑,优先使用任务配置的模型名称
- 记录日志输出所使用的模型名称及对应的任务ID,便于追踪分析过程
2025-10-07 16:07:23 +08:00
砂糖
4cec966613 feat: python模型管理 2025-10-07 15:49:58 +08:00
1a7ecafc7d Merge remote-tracking branch 'origin/master' 2025-10-07 14:29:44 +08:00
735704d585 refactor(scheduler): 优化巡检任务调度逻辑
- 移除任务状态自动规范化逻辑
- 不再修改任务状态字段
- 仅根据任务状态 0=启用、1=停用 控制触发
- 移除运行状态缓存机制- 注释掉任务状态更新相关代码
- 调整任务记录失败状态值为2
2025-10-07 14:29:33 +08:00
33 changed files with 679 additions and 581 deletions

View File

@@ -12,7 +12,7 @@ from app.models import Detection
class PythonModelDetector:
"""Object detector using native Python models"""
def __init__(self, model_name: str, model_path: str, input_width: int, input_height: int, color: int = 0x00FF00):
def __init__(self, model_name: str, model_path: str, input_width: int, input_height: int, color: int = 0x00FF00, model_config: dict = None):
"""
Initialize detector with Python model
@@ -22,11 +22,13 @@ class PythonModelDetector:
input_width: Input width for the model
input_height: Input height for the model
color: RGB color for detection boxes (default: green)
model_config: Additional configuration to pass to the model
"""
self.model_name = model_name
self.input_width = input_width
self.input_height = input_height
self.color = color
self.model_config = model_config or {}
# Convert color from RGB to BGR (OpenCV uses BGR)
self.color_bgr = ((color & 0xFF) << 16) | (color & 0xFF00) | ((color >> 16) & 0xFF)
@@ -72,8 +74,18 @@ class PythonModelDetector:
if not hasattr(model_module, "Model"):
raise AttributeError(f"Model module must define a 'Model' class: {model_path}")
# Create model instance
self.model = model_module.Model()
# Create model instance with config
# Try to pass config to model constructor if it accepts parameters
import inspect
model_class = model_module.Model
sig = inspect.signature(model_class.__init__)
if len(sig.parameters) > 1: # Has parameters beyond 'self'
# Pass all config as keyword arguments
self.model = model_class(**self.model_config)
else:
# No parameters, create without arguments
self.model = model_class()
# Check if model has the required methods
if not hasattr(self.model, "predict"):
@@ -113,18 +125,16 @@ class PythonModelDetector:
# Original image dimensions
img_height, img_width = img.shape[:2]
# Preprocess image
processed_img = self.preprocess(img)
# Measure inference time
start_time = time.time()
try:
# Run inference using model's predict method
# Note: Pass original image to model, let it handle preprocessing
# Expected return format from model's predict:
# List of dicts with keys: 'bbox', 'class_id', 'confidence'
# bbox: (x, y, w, h) normalized [0-1]
model_results = self.model.predict(processed_img)
model_results = self.model.predict(img)
# Calculate inference time in milliseconds
inference_time = (time.time() - start_time) * 1000
@@ -279,13 +289,22 @@ class ModelManager:
# Use color from palette
color = palette[i % len(palette)]
# Extract model-specific config (model_file, model_name, etc.)
# These will be passed to the Model class __init__
model_init_config = {}
if "model_file" in model_config:
model_init_config["model_file"] = model_config["model_file"]
if "display_name" in model_config:
model_init_config["model_name"] = model_config["display_name"]
# Create detector for Python model
detector = PythonModelDetector(
model_name=name,
model_path=path,
input_width=size[0],
input_height=size[1],
color=color
color=color,
model_config=model_init_config
)
self.models[name] = detector

View File

@@ -37,7 +37,7 @@ async def startup_event():
model_manager = ModelManager()
# Look for models.json configuration file
models_json_path = os.getenv("MODELS_JSON", os.path.join(os.path.dirname(__file__), "..", "models", "models.json"))
models_json_path = os.getenv("MODELS_JSON", os.path.join(os.path.dirname(__file__), "..", "models.json"))
if os.path.exists(models_json_path):
try:
@@ -130,8 +130,12 @@ async def detect_file(
file: UploadFile = File(...)
):
"""Detect objects in an uploaded image file"""
global model_manager
print(f"接收到的 model_name: {model_name}")
print(f"文件名: {file.filename}")
print(f"文件内容类型: {file.content_type}")
global model_manager
if not model_manager:
raise HTTPException(status_code=500, detail="Model manager not initialized")
@@ -143,16 +147,39 @@ async def detect_file(
# Read uploaded file
try:
contents = await file.read()
print(f"文件大小: {len(contents)} 字节")
if len(contents) == 0:
raise HTTPException(status_code=400, detail="Empty file")
nparr = np.frombuffer(contents, np.uint8)
print(f"numpy数组形状: {nparr.shape}, dtype: {nparr.dtype}")
image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
if image is None:
raise HTTPException(status_code=400, detail="Invalid image data")
print("错误: cv2.imdecode 返回 None")
raise HTTPException(status_code=400, detail="Invalid image data - failed to decode")
print(f"解码后图像形状: {image.shape}, dtype: {image.dtype}")
except HTTPException:
raise
except Exception as e:
print(f"处理图像时出错: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=400, detail=f"Failed to process image: {str(e)}")
# Run detection
detections, inference_time = detector.detect(image)
try:
detections, inference_time = detector.detect(image)
print(f"检测完成: 找到 {len(detections)} 个目标, 耗时 {inference_time:.2f}ms")
except Exception as e:
print(f"推理过程中出错: {str(e)}")
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Detection failed: {str(e)}")
return DetectionResponse(
model_name=model_name,

View File

@@ -0,0 +1,18 @@
[
{
"name": "smoke",
"path": "models/universal_yolo_model.py",
"model_file": "smoke.pt",
"display_name": "吸烟检测",
"size": [640, 640],
"comment": "吸烟检测模型 - YOLOv11"
},
{
"name": "garbage",
"path": "models/universal_yolo_model.py",
"model_file": "garbage.pt",
"display_name": "垃圾识别",
"size": [640, 640],
"comment": "垃圾检测模型 - YOLOv8"
}
]

View File

@@ -1,207 +0,0 @@
import os
import numpy as np
import cv2
from typing import List, Dict, Any
import torch
class Model:
"""
垃圾识别模型 - 直接加载 PyTorch 模型文件
"""
def __init__(self):
"""初始化模型"""
# 获取当前文件所在目录路径
model_dir = os.path.dirname(os.path.abspath(__file__))
# 模型文件路径
model_path = os.path.join(model_dir, "best.pt")
print(f"正在加载垃圾识别模型: {model_path}")
# 加载 PyTorch 模型
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {self.device}")
# 使用 YOLOv5 或通用方式加载模型
try:
# 尝试使用 YOLOv5 加载
import sys
sys.path.append(os.path.dirname(model_dir)) # 添加父目录到路径
try:
# 方法1: 如果安装了 YOLOv5
import yolov5
self.model = yolov5.load(model_path, device=self.device)
self.yolov5_api = True
print("使用 YOLOv5 包加载模型")
except (ImportError, ModuleNotFoundError):
# 方法2: 直接加载 YOLO 代码
from models.yolov5_utils import attempt_load
self.model = attempt_load(model_path, device=self.device)
self.yolov5_api = False
print("使用内置 YOLOv5 工具加载模型")
except Exception as e:
# 方法3: 通用 PyTorch 加载
print(f"YOLOv5 加载失败: {e}")
print("使用通用 PyTorch 加载")
self.model = torch.load(model_path, map_location=self.device)
if isinstance(self.model, dict) and 'model' in self.model:
self.model = self.model['model']
self.yolov5_api = False
# 如果是 ScriptModule设置为评估模式
if isinstance(self.model, torch.jit.ScriptModule):
self.model.eval()
elif hasattr(self.model, 'eval'):
self.model.eval()
# 加载类别名称
self.classes = []
classes_path = os.path.join(model_dir, "classes.txt")
if os.path.exists(classes_path):
with open(classes_path, 'r', encoding='utf-8') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
print(f"已加载 {len(self.classes)} 个类别")
else:
# 如果模型自带类别信息
if hasattr(self.model, 'names') and self.model.names:
self.classes = self.model.names
print(f"使用模型自带类别,共 {len(self.classes)} 个类别")
else:
print("未找到类别文件,将使用数字索引作为类别名")
# 设置识别参数
self.conf_threshold = 0.25 # 置信度阈值
self.img_size = 640 # 默认输入图像大小
print("垃圾识别模型加载完成")
def preprocess(self, image: np.ndarray) -> np.ndarray:
"""预处理图像"""
# 如果是使用 YOLOv5 API不需要预处理
if hasattr(self, 'yolov5_api') and self.yolov5_api:
return image
# 默认预处理:调整大小并归一化
img = cv2.resize(image, (self.img_size, self.img_size))
# BGR 转 RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 归一化 [0, 255] -> [0, 1]
img = img / 255.0
# HWC -> CHW (高度,宽度,通道 -> 通道,高度,宽度)
img = img.transpose(2, 0, 1)
# 转为 torch tensor
img = torch.from_numpy(img).float()
# 添加批次维度
img = img.unsqueeze(0)
# 移至设备
img = img.to(self.device)
return img
def predict(self, image: np.ndarray) -> List[Dict[str, Any]]:
"""模型推理"""
original_height, original_width = image.shape[:2]
try:
# 如果使用 YOLOv5 API
if hasattr(self, 'yolov5_api') and self.yolov5_api:
# YOLOv5 API 直接处理图像
results = self.model(image)
# 提取检测结果
predictions = results.pred[0] # 第一批次的预测
detections = []
for *xyxy, conf, cls_id in predictions.cpu().numpy():
x1, y1, x2, y2 = xyxy
# 转换为归一化坐标 (x, y, w, h)
x = x1 / original_width
y = y1 / original_height
w = (x2 - x1) / original_width
h = (y2 - y1) / original_height
# 整数类别 ID
cls_id = int(cls_id)
# 获取类别名称
class_name = f"cls{cls_id}"
if 0 <= cls_id < len(self.classes):
class_name = self.classes[cls_id]
# 添加检测结果
if conf >= self.conf_threshold:
detections.append({
'bbox': (x, y, w, h),
'class_id': cls_id,
'confidence': float(conf)
})
return detections
else:
# 通用 PyTorch 模型处理
# 预处理图像
img = self.preprocess(image)
# 推理
with torch.no_grad():
outputs = self.model(img)
# 后处理结果(这里需要根据模型输出格式调整)
detections = []
# 假设输出格式是 YOLO 风格:[batch_idx, x1, y1, x2, y2, conf, cls_id]
if isinstance(outputs, torch.Tensor) and outputs.dim() == 2 and outputs.size(1) >= 6:
for *xyxy, conf, cls_id in outputs.cpu().numpy():
if conf >= self.conf_threshold:
x1, y1, x2, y2 = xyxy
# 转换为归一化坐标 (x, y, w, h)
x = x1 / original_width
y = y1 / original_height
w = (x2 - x1) / original_width
h = (y2 - y1) / original_height
# 整数类别 ID
cls_id = int(cls_id)
detections.append({
'bbox': (x, y, w, h),
'class_id': cls_id,
'confidence': float(conf)
})
# 处理其他可能的输出格式
else:
# 这里需要根据模型的实际输出格式进行适配
print("警告:无法识别的模型输出格式,请检查模型类型")
return detections
except Exception as e:
print(f"推理过程中出错: {str(e)}")
# 出错时返回空结果
return []
@property
def applies_nms(self) -> bool:
"""模型是否内部应用了 NMS"""
# YOLOv5 会自动应用 NMS
return True
def close(self):
"""释放资源"""
if hasattr(self, 'model'):
# 删除模型以释放 GPU 内存
del self.model
if torch.cuda.is_available():
torch.cuda.empty_cache()
print("垃圾识别模型已关闭")

View File

@@ -1,8 +0,0 @@
[
{
"name": "yolov8_detector",
"path": "models/yolov8_model.py",
"size": [640, 640],
"comment": "YOLOv8检测模型确保将训练好的best.pt文件放在models目录下"
}
]

Binary file not shown.

View File

@@ -0,0 +1 @@
垃圾

View File

@@ -1,126 +0,0 @@
import numpy as np
import cv2
from typing import List, Dict, Any, Tuple
class Model:
"""
Smoke detection model implementation
This is a simple example that could be replaced with an actual
TensorFlow, PyTorch, or other ML framework implementation.
"""
def __init__(self):
"""Initialize smoke detection model"""
# In a real implementation, you would load your model here
print("Smoke detection model initialized")
# Define smoke class IDs
self.smoke_classes = {
0: "smoke",
1: "fire"
}
def preprocess(self, image: np.ndarray) -> np.ndarray:
"""Preprocess image for model input"""
# Convert BGR to grayscale for smoke detection
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Convert back to 3 channels to match model expected input shape
gray_3ch = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
# In a real implementation, you would do normalization, etc.
return gray_3ch
def predict(self, image: np.ndarray) -> List[Dict[str, Any]]:
"""
Run smoke detection on the image
This is a simplified example that uses basic image processing
In a real implementation, you would use your ML model
"""
# Convert to grayscale for processing
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Apply Gaussian blur to reduce noise
blurred = cv2.GaussianBlur(gray, (15, 15), 0)
# Simple thresholding to find potential smoke regions
# In a real implementation, you'd use a trained model
_, thresh = cv2.threshold(blurred, 100, 255, cv2.THRESH_BINARY)
# Find contours in the thresholded image
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Process contours to find potential smoke regions
detections = []
height, width = image.shape[:2]
for contour in contours:
# Get bounding box
x, y, w, h = cv2.boundingRect(contour)
# Filter small regions
if w > width * 0.05 and h > height * 0.05:
# Calculate area ratio
area = cv2.contourArea(contour)
rect_area = w * h
fill_ratio = area / rect_area if rect_area > 0 else 0
# Smoke tends to have irregular shapes
# This is just for demonstration purposes
if fill_ratio > 0.2 and fill_ratio < 0.8:
# Normalize coordinates
x_norm = x / width
y_norm = y / height
w_norm = w / width
h_norm = h / height
# Determine if it's smoke or fire (just a simple heuristic for demo)
# In a real model, this would be determined by the model prediction
class_id = 0 # Default to smoke
# Check if the region has high red values (fire)
roi = image[y:y+h, x:x+w]
if roi.size > 0: # Make sure ROI is not empty
avg_color = np.mean(roi, axis=(0, 1))
if avg_color[2] > 150 and avg_color[2] > avg_color[0] * 1.5: # High red, indicating fire
class_id = 1 # Fire
# Calculate confidence based on fill ratio
# This is just for demonstration
confidence = 0.5 + fill_ratio * 0.3
# Add to detections
detections.append({
'bbox': (x_norm, y_norm, w_norm, h_norm),
'class_id': class_id,
'confidence': confidence
})
# For demo purposes, if no smoke detected by algorithm,
# add a small chance of random detection
if not detections and np.random.random() < 0.1: # 10% chance
# Random smoke detection
x = np.random.random() * 0.7
y = np.random.random() * 0.7
w = 0.1 + np.random.random() * 0.2
h = 0.1 + np.random.random() * 0.2
confidence = 0.5 + np.random.random() * 0.3
detections.append({
'bbox': (x, y, w, h),
'class_id': 0, # Smoke
'confidence': confidence
})
return detections
@property
def applies_nms(self) -> bool:
"""Model does not apply NMS internally"""
return False
def close(self):
"""Release resources"""
# In a real implementation, you would release model resources here
pass

View File

@@ -6,17 +6,39 @@ import torch
class Model:
"""
YOLOv8 模型包装类 - 使用 Ultralytics YOLO
通用 YOLO 模型 - 支持 YOLOv8/YOLOv11 等基于 Ultralytics 的模型
"""
def __init__(self):
"""初始化YOLOv8模型"""
def __init__(self, model_file: str = None, model_name: str = "YOLO"):
"""
初始化模型
Args:
model_file: 模型文件名 smoke.pt, best.pt
model_name: 模型显示名称用于日志
"""
# 获取当前文件所在目录路径
model_dir = os.path.dirname(os.path.abspath(__file__))
# 模型文件路径
model_path = os.path.join(model_dir, "best.pt")
print(f"正在加载YOLOv8模型: {model_path}")
# 如果没有指定模型文件,尝试常见的文件名
if model_file is None:
for possible_file in ['garbage.pt', 'smoke.pt', 'best.pt', 'yolov8.pt', 'model.pt']:
test_path = os.path.join(model_dir, possible_file)
if os.path.exists(test_path):
model_file = possible_file
break
if model_file is None:
raise FileNotFoundError(f"未找到模型文件,请在初始化时指定 model_file 参数")
# 模型文件路径
model_path = os.path.join(model_dir, model_file)
if not os.path.exists(model_path):
raise FileNotFoundError(f"模型文件不存在: {model_path}")
self.model_name = model_name
print(f"正在加载{model_name}模型: {model_path}")
# 检查设备
self.device = "cuda" if torch.cuda.is_available() else "cpu"
@@ -26,19 +48,30 @@ class Model:
try:
from ultralytics import YOLO
self.model = YOLO(model_path)
print("使用 Ultralytics YOLO 加载模型成功")
print(f"使用 Ultralytics YOLO 加载模型成功")
except ImportError:
raise ImportError("请安装 ultralytics: pip install ultralytics>=8.0.0")
except Exception as e:
raise Exception(f"加载YOLOv8模型失败: {str(e)}")
raise Exception(f"加载{model_name}模型失败: {str(e)}")
# 加载类别名称
self.classes = []
classes_path = os.path.join(model_dir, "classes.txt")
if os.path.exists(classes_path):
with open(classes_path, 'r', encoding='utf-8') as f:
# 1. 首先尝试加载与模型文件同名的类别文件(如 smoke.txt
model_base_name = os.path.splitext(model_file)[0]
classes_path_specific = os.path.join(model_dir, f"{model_base_name}.txt")
# 2. 然后尝试加载通用的 classes.txt
classes_path_generic = os.path.join(model_dir, "classes.txt")
if os.path.exists(classes_path_specific):
with open(classes_path_specific, 'r', encoding='utf-8') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
print(f"已加载 {len(self.classes)} 个类别")
print(f"已加载类别文件: {model_base_name}.txt ({len(self.classes)} 个类别)")
elif os.path.exists(classes_path_generic):
with open(classes_path_generic, 'r', encoding='utf-8') as f:
self.classes = [line.strip() for line in f.readlines() if line.strip()]
print(f"已加载类别文件: classes.txt ({len(self.classes)} 个类别)")
else:
# 使用模型自带的类别信息
if hasattr(self.model, 'names') and self.model.names:
@@ -51,10 +84,10 @@ class Model:
self.conf_threshold = 0.25 # 置信度阈值
self.img_size = 640 # 默认输入图像大小
print("YOLOv8模型加载完成")
print(f"{model_name}模型加载完成")
def preprocess(self, image: np.ndarray) -> np.ndarray:
"""预处理图像 - YOLOv8会自动处理,这里直接返回"""
"""预处理图像 - Ultralytics YOLO 会自动处理,这里直接返回"""
return image
def predict(self, image: np.ndarray) -> List[Dict[str, Any]]:
@@ -62,7 +95,7 @@ class Model:
original_height, original_width = image.shape[:2]
try:
# YOLOv8推理
# YOLO 推理
results = self.model(
image,
conf=self.conf_threshold,
@@ -122,7 +155,7 @@ class Model:
@property
def applies_nms(self) -> bool:
"""模型是否内部应用了 NMS"""
# YOLOv8会自动应用 NMS
# Ultralytics YOLO 会自动应用 NMS
return True
def close(self):
@@ -132,4 +165,5 @@ class Model:
del self.model
if torch.cuda.is_available():
torch.cuda.empty_cache()
print("YOLOv8模型已关闭")
print(f"{self.model_name}模型已关闭")

View File

@@ -1,56 +0,0 @@
import torch
import torch.nn as nn
import sys
import os
def attempt_load(weights, device=''):
"""尝试加载YOLOv5模型"""
# 加载模型
model = torch.load(weights, map_location=device)
# 确定模型格式
if isinstance(model, dict):
if 'model' in model: # state_dict格式
model = model['model']
elif 'state_dict' in model: # state_dict格式
model = model['state_dict']
# 如果是state_dict则需要创建模型架构
if isinstance(model, dict):
print("警告:加载的是权重字典,尝试创建默认模型结构")
from models.yolov5_model import YOLOv5
model_arch = YOLOv5()
model_arch.load_state_dict(model)
model = model_arch
# 设置为评估模式
if isinstance(model, nn.Module):
model.eval()
# 检查是否有类别信息
if not hasattr(model, 'names') or not model.names:
print("模型没有类别信息,尝试加载默认类别")
# 设置通用类别
model.names = ['object']
return model
class YOLOv5:
"""简化版YOLOv5模型结构用于加载权重"""
def __init__(self):
super(YOLOv5, self).__init__()
self.names = [] # 类别名称
# 这里应该添加真实的网络结构
# 但为了简单起见,我们只提供一个占位符
# 在实际使用中,您应该实现完整的网络架构
def forward(self, x):
# 这里应该是实际的前向传播逻辑
# 这只是一个占位符
raise NotImplementedError("这是一个占位符模型请使用完整的YOLOv5模型实现")
def load_state_dict(self, state_dict):
print("尝试加载模型权重")
# 实际的权重加载逻辑
# 这只是一个占位符
return self

View File

@@ -21,8 +21,8 @@ export function getAlarm(alarmId) {
export function handleAlarm(data) {
return request({
url: '/video/alarm/handle',
method: 'put',
data: data
method: 'post',
params: data
})
}
@@ -30,7 +30,7 @@ export function handleAlarm(data) {
export function batchHandleAlarm(data) {
return request({
url: '/video/alarm/batchHandle',
method: 'put',
method: 'post',
data: data
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -19,7 +19,6 @@
<div @click="router.push('/tool/gen')" title="代码生成" class="right-menu-item hover-effect">
<svg-icon icon-class="code" />
</div>
<screenfull id="screenfull" class="right-menu-item hover-effect" />

View File

@@ -15,19 +15,26 @@
</el-form>
<div style="display: flex;">
<div style="display: flex; gap: 20px">
<div class="card" @click="handleVideoCameraFilled(item)" v-for="item in deviceList" :key="item.deviceId">
<div class="card-image">
<img :src="item.url" :alt="item.url" style="width: 100%; height: 100%;" />
<!-- <div style="width: 100%; height: 100%;">
{{ item.url }}
</div> -->
<el-row>
<el-col :span="16">
<h3 style="display: flex; align-items: center;"><img :src="DeviceImage" alt="" style="width: 20px; height: 20px; margin-right: 10px;"> {{ item.deviceName }}</h3>
<div class="li" style="display: flex; align-items: center;">设备分类<dict-tag :options="device_type" :value="item.type" /></div>
<div class="li" style="display: flex; align-items: center;">设备IP{{ item.ip }}</div>
<div class="li" style="display: flex; align-items: center;">用户名{{ item.username ?? '-' }}</div>
</el-col>
<el-col :span="8">
<img :src="CameraImage" alt="设备图片" style="width: 100%;" />
</el-col>
</el-row>
<!-- <div class="card-image">
<img :src="DeviceImage" :alt="item.url" style="width: 100%; height: 100%;" />
</div>
<div class="category"><dict-tag :options="device_type" :value="item.type" /></div>
<div class="heading">
{{ item.ip }}
<!-- <div class="author"> By <span class="name">Abi</span> 4 days ago</div> -->
</div>
</div> -->
</div>
</div>
@@ -89,6 +96,8 @@ import { listDevice } from "@/api/video/device";
import { listInspectionRecord } from "@/api/video/insRecord";
import { listInspection } from "@/api/video/inspection";
import { listAlarm } from "@/api/video/alarm";
import DeviceImage from "@/assets/images/device.png";
import CameraImage from "@/assets/images/camera.png";
const { proxy } = getCurrentInstance();
const { device_on_status, device_type, ins_record_status } = proxy.useDict('device_on_status', 'device_type', 'ins_record_status');
@@ -186,19 +195,17 @@ getList();
<style scoped>
/* From Uiverse.io by alexmaracinaru */
.card {
width: 190px;
background: white;
width: 300px;
padding: .4em;
border-radius: 6px;
border-radius: 0;
border: 1px solid #e0e0e0;
}
.card-image {
background-color: rgb(236, 236, 236);
width: 100%;
height: 130px;
text-align: center;
word-break: break-all;
border-radius: 6px 6px 0 0;
}
.card-image:hover {

View File

@@ -84,10 +84,9 @@
<el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="报警ID" align="center" prop="alarmId" />
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="报警类型" align="center" prop="alarmType" />
<!-- <el-table-column label="报警类型" align="center" prop="alarmType" /> -->
<el-table-column label="报警级别" align="center" prop="alarmLevel">
<template #default="scope">
<el-tag v-if="scope.row.alarmLevel === '1'" type="info"></el-tag>
@@ -109,20 +108,10 @@
:src="scope.row.imagePath"
:preview-src-list="[scope.row.imagePath]"
fit="cover"
preview-teleported
/>
</template>
</el-table-column>
<el-table-column label="报警视频" align="center" prop="videoPath">
<template #default="scope">
<el-button
v-if="scope.row.videoPath"
link
type="primary"
icon="VideoPlay"
@click="handleView(scope.row)"
>播放</el-button>
</template>
</el-table-column>
<el-table-column label="报警时间" align="center" prop="alarmTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
@@ -200,10 +189,9 @@
<!-- 查看详情对话框 -->
<el-dialog title="报警详情" v-model="viewOpen" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="报警ID">{{ viewForm.alarmId }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ viewForm.taskName }}</el-descriptions-item>
<el-descriptions-item label="设备名称">{{ viewForm.deviceName }}</el-descriptions-item>
<el-descriptions-item label="报警类型">{{ viewForm.alarmType }}</el-descriptions-item>
<!-- <el-descriptions-item label="报警类型">{{ viewForm.alarmType }}</el-descriptions-item> -->
<el-descriptions-item label="报警级别">
<el-tag v-if="viewForm.alarmLevel === '1'" type="info"></el-tag>
<el-tag v-else-if="viewForm.alarmLevel === '2'" type="warning"></el-tag>
@@ -233,15 +221,7 @@
:src="viewForm.imagePath"
:preview-src-list="[viewForm.imagePath]"
fit="contain"
/>
</div>
<div v-if="viewForm.videoPath" style="margin-top: 20px;">
<h4>报警视频</h4>
<video
:src="viewForm.videoPath"
style="width: 100%; max-height: 420px; background: #000"
controls
preload="metadata"
preview-teleported
/>
</div>
</el-dialog>

View File

@@ -63,6 +63,7 @@
<el-table-column type="selection" width="55" align="center" />
<!-- <el-table-column label="设备ID" align="center" prop="deviceId" />-->
<el-table-column label="IP地址" align="center" prop="ip" width="150" fixed />
<el-table-column label="设备名称" align="center" prop="deviceName" width="150" />
<el-table-column label="设备类型" align="center" prop="type" width="80">
<template #default="scope">
<dict-tag :options="device_type" :value="scope.row.type"/>
@@ -103,6 +104,9 @@
<el-form-item label="IP地址" prop="ip">
<el-input v-model="form.ip" placeholder="请输入IP地址" />
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="form.deviceName" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item label="设备类型" prop="type">
<el-select v-model="form.type" placeholder="请选择设备类型">
<el-option
@@ -218,6 +222,7 @@ function reset() {
form.value = {
deviceId: null,
ip: null,
deviceName: null,
type: null,
userName: null,
password: null,

View File

@@ -12,6 +12,11 @@
<el-option v-for="dict in task_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="模型" prop="modelName">
<el-select v-model="queryParams.modelName" placeholder="请选择模型" clearable>
<el-option v-for="dict in py_model_manager" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -40,8 +45,14 @@
<el-table v-loading="loading" :data="inspectionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="任务ID" align="center" prop="taskId" />
<!-- <el-table-column label="任务ID" align="center" prop="taskId" /> -->
<el-table-column label="模型" align="center" prop="modelName">
<template #default="scope">
<dict-tag :options="py_model_manager" :value="scope.row.modelName" />
</template>
</el-table-column>
<el-table-column label="任务名称" align="center" prop="taskName" />
<el-table-column label="设备IP" align="center" prop="deviceIp" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="Cron表达式" align="center" prop="cronExpression" />
<el-table-column label="巡检时长(秒)" align="center" prop="duration" />
@@ -90,6 +101,11 @@
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="form.taskName" placeholder="请输入任务名称" />
</el-form-item>
<el-form-item label="模型" prop="modelName">
<el-select v-model="form.modelName" placeholder="请选择模型" style="width: 100%">
<el-option v-for="dict in py_model_manager" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="设备" prop="deviceId">
<el-select v-model="form.deviceId" placeholder="请选择设备" style="width: 100%">
<el-option v-for="device in deviceList" :key="device.deviceId" :label="device.ip"
@@ -146,7 +162,7 @@ import Crontab from '@/components/Crontab'
import router from '@/router'
const { proxy } = getCurrentInstance();
const { sys_normal_disable, task_status } = proxy.useDict('sys_normal_disable', 'task_status');
const { sys_normal_disable, task_status, py_model_manager } = proxy.useDict('sys_normal_disable', 'task_status', 'py_model_manager');
const inspectionList = ref([]);
const deviceList = ref([]);

View File

@@ -0,0 +1,323 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="模型简称" prop="dictLabel">
<el-input
v-model="queryParams.dictLabel"
placeholder="请输入模型简称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="数据状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:dict:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['system:dict:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:dict:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['system:dict:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="模型简称" align="center" prop="dictLabel">
<template #default="scope">
<span v-if="(scope.row.listClass == '' || scope.row.listClass == 'default') && (scope.row.cssClass == '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
<el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
</template>
</el-table-column>
<el-table-column label="模型名称" align="center" prop="dictValue" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改参数配置对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="模型简称" prop="dictLabel">
<el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
</el-form-item>
<el-form-item label="模型名称" prop="dictValue">
<el-input v-model="form.dictValue" placeholder="请输入模型名称" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Data">
import useDictStore from '@/store/modules/dict'
import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type";
import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data";
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const dataList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const defaultDictType = ref("");
const typeOptions = ref([]);
const route = useRoute();
// 数据标签回显样式
const listClassOptions = ref([
{ value: "default", label: "默认" },
{ value: "primary", label: "主要" },
{ value: "success", label: "成功" },
{ value: "info", label: "信息" },
{ value: "warning", label: "警告" },
{ value: "danger", label: "危险" }
]);
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 20,
dictType: undefined,
dictLabel: undefined,
status: undefined
},
rules: {
dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询字典类型详细 */
function getTypes(dictId) {
getType(dictId).then(response => {
queryParams.value.dictType = response.data.dictType;
defaultDictType.value = response.data.dictType;
getList();
});
}
/** 查询字典类型列表 */
function getTypeList() {
getDictOptionselect().then(response => {
typeOptions.value = response.data;
});
}
/** 查询字典数据列表 */
function getList() {
loading.value = true;
listData(queryParams.value).then(response => {
dataList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
dictCode: undefined,
dictLabel: undefined,
dictValue: undefined,
cssClass: undefined,
listClass: "default",
dictSort: 0,
status: "0",
remark: undefined
};
proxy.resetForm("dataRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 返回按钮操作 */
function handleClose() {
const obj = { path: "/system/dict" };
proxy.$tab.closeOpenPage(obj);
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
queryParams.value.dictType = defaultDictType.value;
handleQuery();
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加字典数据";
form.value.dictType = queryParams.value.dictType;
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.dictCode);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const dictCode = row.dictCode || ids.value;
getData(dictCode).then(response => {
form.value = response.data;
open.value = true;
title.value = "修改字典数据";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["dataRef"].validate(valid => {
if (valid) {
if (form.value.dictCode != undefined) {
updateData(form.value).then(response => {
useDictStore().removeDict(queryParams.value.dictType);
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
addData(form.value).then(response => {
useDictStore().removeDict(queryParams.value.dictType);
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const dictCodes = row.dictCode || ids.value;
proxy.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?').then(function() {
return delData(dictCodes);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
useDictStore().removeDict(queryParams.value.dictType);
}).catch(() => {});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download("system/dict/data/export", {
...queryParams.value
}, `dict_data_${new Date().getTime()}.xlsx`);
}
getTypes(105);
getTypeList();
</script>

View File

@@ -109,6 +109,10 @@
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-quartz</artifactId>
</dependency>
</dependencies>

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.video.domain.AlarmRecord;
import com.ruoyi.video.domain.bo.alarmBatchBo;
import com.ruoyi.video.service.InspectionTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -34,7 +35,6 @@ public class AlarmRecordController extends BaseController {
/**
* 查询报警记录列表
*/
@PreAuthorize("@ss.hasPermi('video:alarm:list')")
@GetMapping("/list")
public TableDataInfo list(AlarmRecord alarmRecord) {
startPage();
@@ -45,7 +45,6 @@ public class AlarmRecordController extends BaseController {
/**
* 导出报警记录列表
*/
@PreAuthorize("@ss.hasPermi('video:alarm:export')")
@Log(title = "报警记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AlarmRecord alarmRecord) {
@@ -57,7 +56,6 @@ public class AlarmRecordController extends BaseController {
/**
* 处理报警记录
*/
@PreAuthorize("@ss.hasPermi('video:alarm:handle')")
@Log(title = "处理报警记录", businessType = BusinessType.UPDATE)
@PostMapping("/handle")
public AjaxResult handle(@RequestParam Long alarmId,
@@ -71,12 +69,12 @@ public class AlarmRecordController extends BaseController {
/**
* 批量处理报警记录
*/
@PreAuthorize("@ss.hasPermi('video:alarm:handle')")
@Log(title = "批量处理报警记录", businessType = BusinessType.UPDATE)
@PostMapping("/batchHandle")
public AjaxResult batchHandle(@RequestParam Long[] alarmIds,
@RequestParam String handleStatus,
@RequestParam(required = false) String handleRemark) {
public AjaxResult batchHandle(@RequestBody alarmBatchBo alarmBatchBo) {
Long[] alarmIds = alarmBatchBo.getAlarmIds();
String handleStatus = alarmBatchBo.getHandleStatus();
String handleRemark = alarmBatchBo.getHandleRemark();
String handleBy = SecurityUtils.getUsername();
int successCount = 0;
for (Long alarmId : alarmIds) {

View File

@@ -14,6 +14,8 @@ public class Device extends BaseEntity {
private Long deviceId;
@Excel(name = "IP地址")
private String ip;
@Excel(name = "设备名称")
private String deviceName;
@Excel(
name = "设备类型(1=haikan,2=dahua)"
)
@@ -51,7 +53,7 @@ public class Device extends BaseEntity {
}
public String toString() {
return "Device{deviceId=" + this.deviceId + ", ip='" + this.ip + "', type='" + this.type + "', mediaKey='" + this.mediaKey + "', userName='" + this.userName + "', password='" + this.password + "', url='" + this.url + "', enabledFlv='" + this.enabledFlv + "', enabledHls='" + this.enabledHls + "', mode='" + this.mode + "'}";
return "Device{deviceId=" + this.deviceId + ", deviceName='" + this.deviceName + "', ip='" + this.ip + "', type='" + this.type + "', mediaKey='" + this.mediaKey + "', userName='" + this.userName + "', password='" + this.password + "', url='" + this.url + "', enabledFlv='" + this.enabledFlv + "', enabledHls='" + this.enabledHls + "', mode='" + this.mode + "'}";
}
public Long getDeviceId() {
@@ -62,6 +64,14 @@ public class Device extends BaseEntity {
this.deviceId = deviceId;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getIp() {
return this.ip;
}

View File

@@ -6,7 +6,7 @@ import java.util.Date;
/**
* 巡检任务对象 v_inspection_task
*
*
* @author ruoyi
* @date 2025-09-27
*/
@@ -22,9 +22,12 @@ public class InspectionTask extends BaseEntity {
/** 设备ID */
private Long deviceId;
/** 设备名称 */
/** 设备名称(联查返回) */
private String deviceName;
/** 设备IP联查返回 */
private String deviceIp;
/** Cron表达式 */
private String cronExpression;
@@ -52,6 +55,16 @@ public class InspectionTask extends BaseEntity {
/** 报警次数 */
private Long alarmCount;
private String modelName;
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getModelName() {
return modelName;
}
public void setTaskId(Long taskId) {
this.taskId = taskId;
}
@@ -84,6 +97,14 @@ public class InspectionTask extends BaseEntity {
return deviceName;
}
public String getDeviceIp() {
return deviceIp;
}
public void setDeviceIp(String deviceIp) {
this.deviceIp = deviceIp;
}
public void setCronExpression(String cronExpression) {
this.cronExpression = cronExpression;
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.video.domain.bo;
import lombok.Data;
@Data
public class alarmBatchBo {
private Long[] alarmIds;
private String handleStatus;
private String handleRemark;
}

View File

@@ -24,9 +24,10 @@ import java.util.concurrent.TimeUnit;
/**
* 内部巡检任务调度器不依赖若依Quartz
* - 周期性轮询 v_inspection_task 中启用(status='0')的任务
* - 周期性轮询 v_inspection_task 中任务,按状态 0=启用、1=停用 控制是否触发
* - 使用 Spring CronExpression 计算下一次执行
* - 到点调用 InspectionTaskService.executeInspectionTask(taskId)
* - 不修改任务的状态字段(状态仅由外部手动启停控制)
*/
@Slf4j
@Component
@@ -119,21 +120,6 @@ public class InspectionCronScheduler {
} catch (Exception e) {
log.debug("更新nextExecuteTime失败: taskId={}, err={}", taskId, e.getMessage());
}
// 若发现状态为“2”服务执行完成后置状态将其规范化回“0”以维持启用
if ("2".equals(status)) {
try {
InspectionTask patch = new InspectionTask();
patch.setTaskId(taskId);
patch.setStatus("0");
patch.setUpdateTime(DateUtils.getNowDate());
inspectionTaskMapper.updateInspectionTask(patch);
task.setStatus("0");
} catch (Exception e) {
log.debug("规范化任务状态失败: taskId={}, err={}", taskId, e.getMessage());
}
}
// 到点触发判定:如果 next <= now + 10s 窗口内,就触发一次
long nowMs = System.currentTimeMillis();
long nextMs = nextDate.getTime();
@@ -164,19 +150,6 @@ public class InspectionCronScheduler {
} catch (Exception e) {
log.error("执行巡检任务触发失败: taskId={}, err={}", taskId, e.getMessage(), e);
}
// 延迟规范化状态为启用服务执行期间可能将其改为1或2
try {
poller.schedule(() -> {
try {
InspectionTask patch2 = new InspectionTask();
patch2.setTaskId(taskId);
patch2.setStatus("0");
patch2.setUpdateTime(DateUtils.getNowDate());
inspectionTaskMapper.updateInspectionTask(patch2);
} catch (Exception ignored) {}
}, 3, TimeUnit.SECONDS);
} catch (Exception ignore) {}
}
}
} catch (Exception e) {

View File

@@ -64,15 +64,16 @@ public class VideoAnalysisService {
@Autowired
private com.ruoyi.video.mapper.InspectionTaskRecordMapper inspectionTaskRecordMapper;
@Autowired
private InspectionTaskMapper inspectionTaskMapper;
// 检测器配置 - 使用容器名而不是localhost
private static final String PYTHON_API_URL = "http://rtsp-python-service:10083/api/detect/file";
private static final String MODEL_NAME = "yolov8_detector";
// 检测器配置 - 支持环境变量配置
private static final String PYTHON_API_URL = System.getenv().getOrDefault("PYTHON_API_URL", "http://localhost:8000") + "/api/detect/file";
private static final String MODEL_NAME = "smoke"; // 默认使用吸烟检测模型
/**
* 分析视频并更新记录(同步调用)
* @param task 巡检任务
* @param record 巡检记录
* @param videoFile 视频文件
*/
public void analyzeVideoWithRecord(InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record, File videoFile) {
@@ -83,7 +84,11 @@ public class VideoAnalysisService {
File outputVideoFile = File.createTempFile("analysis_output_", ".mp4");
// 创建检测器
HttpYoloDetector detector = new HttpYoloDetector("yolov8", PYTHON_API_URL, MODEL_NAME, 0x00FF00);
String chosenModel = (task.getModelName() != null && !task.getModelName().trim().isEmpty())
? task.getModelName().trim()
: MODEL_NAME;
log.info("使用模型进行分析: {} (taskId={})", chosenModel, task.getTaskId());
HttpYoloDetector detector = new HttpYoloDetector("yolov8", PYTHON_API_URL, chosenModel, 0x00FF00);
// 处理视频并记录检测结果
String detectionResult = processVideoWithRecord(videoFile, outputVideoFile, detector, task, record);
@@ -222,6 +227,9 @@ public class VideoAnalysisService {
*/
private void createAlarmRecordForRecord(InspectionTask task, com.ruoyi.video.domain.InspectionTaskRecord record,
Detection detection, Mat frame, long frameCount) throws Exception {
//创建记录之前应该给task加上报警次数
task.setAlarmCount(task.getAlarmCount() + 1);
inspectionTaskMapper.updateInspectionTask(task);
// 创建告警图像临时文件
File alarmImageFile = File.createTempFile("alarm_", ".jpg");

View File

@@ -2,6 +2,7 @@ package com.ruoyi.video.service.impl;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.quartz.util.CronUtils;
import com.ruoyi.video.domain.*;
import com.ruoyi.video.domain.dto.CameraDto;
import com.ruoyi.video.mapper.InspectionTaskMapper;
@@ -64,7 +65,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
private IVMinioObjectService vMinioObjectService;
// 运行状态缓存(避免重复执行)
private final Map<Long, Boolean> runningTasks = new ConcurrentHashMap<>();
// private final Map<Long, Boolean> runningTasks = new ConcurrentHashMap<>();
private ModelManager modelManager;
// 延迟初始化,避免启动时的依赖问题
@@ -83,16 +84,15 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
@Override
public int insertInspectionTask(InspectionTask inspectionTask) {
inspectionTask.setCreateTime(DateUtils.getNowDate());
// 这些字段在新版实体类中可能不存在,需要进行调整
// inspectionTask.setExecuteCount(0L);
// inspectionTask.setAlarmCount(0L);
inspectionTask.setExecuteCount(0L);
inspectionTask.setAlarmCount(0L);
// 获取设备信息
Device device = deviceService.selectDeviceByDeviceId(inspectionTask.getDeviceId());
// 新版实体类可能不需要设备名称
// if (device != null) {
// inspectionTask.setDeviceName(device.getIp());
// }
// Device device = deviceService.selectDeviceByDeviceId(inspectionTask.getDeviceId());
//
// if (device != null) {
// inspectionTask.setDeviceName(device.getIp());
// }
return inspectionTaskMapper.insertInspectionTask(inspectionTask);
}
@@ -117,14 +117,14 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
if (task == null) {
return false;
}
// 启用任务,使用新版实体类的方法
task.setStatus("0"); // 0表示启用
task.setUpdateTime(DateUtils.getNowDate());
inspectionTaskMapper.updateInspectionTask(task);
runningTasks.put(taskId, true);
// runningTasks.put(taskId, true);
// 这里应该集成到Quartz定时任务中
log.info("启动巡检任务: {}", taskId);
return true;
@@ -142,7 +142,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
task.setUpdateTime(DateUtils.getNowDate());
inspectionTaskMapper.updateInspectionTask(task);
runningTasks.remove(taskId);
// runningTasks.remove(taskId);
log.info("停止巡检任务: {}", taskId);
return true;
@@ -170,7 +170,12 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
try {
// 更新任务状态为执行中
task.setStatus("1");
// task.setStatus("1");
// 更新最后执行时间和下次执行时间
task.setLastExecuteTime(new Date());
task.setNextExecuteTime(CronUtils.getNextExecution(task.getCronExpression()));
//更新执行次数
task.setExecuteCount(task.getExecuteCount() + 1);
inspectionTaskMapper.updateInspectionTask(task);
// 获取设备信息
@@ -191,16 +196,16 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
// 更新任务状态为已完成
task.setStatus("2");
inspectionTaskMapper.updateInspectionTask(task);
// task.setStatus("0");
// inspectionTaskMapper.updateInspectionTask(task);
} catch (Exception e) {
log.error("巡检任务执行失败: taskId={}", taskId, e);
updateRecordFailed(record, e.getMessage());
// 更新任务状态为已完成(虽然失败)
task.setStatus("2");
inspectionTaskMapper.updateInspectionTask(task);
// task.setStatus("0");
// inspectionTaskMapper.updateInspectionTask(task);
}
}
@@ -208,7 +213,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
* 更新记录为失败状态
*/
private void updateRecordFailed(InspectionTaskRecord record, String errorMsg) {
record.setStatus(1); // 失败
record.setStatus(2); // 失败
record.setResult("执行失败: " + errorMsg);
inspectionTaskRecordMapper.updateInspectionTaskRecord(record);
}
@@ -452,7 +457,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
alarm.setTaskId(task.getTaskId());
alarm.setTaskName(task.getTaskName());
alarm.setDeviceId(task.getDeviceId());
alarm.setDeviceName(task.getDeviceName());
// alarm.setDeviceName(task.getDeviceName());
alarm.setAlarmType(best.getLabel());
// 这里需要转换double为float
alarm.setAlarmLevel(getAlarmLevel((float)best.getConfidence()));

View File

@@ -27,14 +27,14 @@ import static org.bytedeco.opencv.global.opencv_imgcodecs.imencode;
*/
public class HttpYoloDetector implements YoloDetector {
private static final Logger log = LoggerFactory.getLogger(HttpYoloDetector.class);
private final String name;
private final String apiUrl;
private final String modelName;
private final int colorBGR;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
/**
* 创建HTTP检测器
* @param name 检测器名称
@@ -49,48 +49,60 @@ public class HttpYoloDetector implements YoloDetector {
this.colorBGR = colorBGR;
this.restTemplate = new RestTemplate();
this.objectMapper = new ObjectMapper();
log.info("创建HTTP YOLOv8检测器: {}, 服务地址: {}, 模型: {}", name, apiUrl, modelName);
}
@Override
public String name() {
return name;
}
@Override
public List<Detection> detect(Mat bgr) {
if (bgr == null || bgr.empty()) {
return Collections.emptyList();
}
try {
// 将OpenCV的Mat转换为JPEG字节数组
BytePointer buffer = new BytePointer();
imencode(".jpg", bgr, buffer);
byte[] jpgBytes = new byte[(int)(buffer.capacity())];
buffer.get(jpgBytes);
buffer.deallocate();
// 准备HTTP请求参数
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("model_name", modelName);
// 仅发送文件model_name 放到查询参数
body.add("file", new CustomByteArrayResource(jpgBytes, "image.jpg"));
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
// 将 model_name 作为查询参数
String urlWithQuery;
try {
String encoded = java.net.URLEncoder.encode(modelName, java.nio.charset.StandardCharsets.UTF_8.toString());
urlWithQuery = apiUrl + (apiUrl.contains("?") ? "&" : "?") + "model_name=" + encoded;
} catch (Exception ex) {
urlWithQuery = apiUrl + (apiUrl.contains("?") ? "&" : "?") + "model_name=" + modelName;
}
// 发送请求到Python服务
ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, requestEntity, String.class);
ResponseEntity<String> response = restTemplate.postForEntity(urlWithQuery, requestEntity, String.class);
String responseBody = response.getBody();
if (!response.getStatusCode().is2xxSuccessful()) {
log.error("HTTP检测失败: status={}, body={}", response.getStatusCodeValue(), responseBody);
return Collections.emptyList();
}
if (responseBody != null) {
// 解析响应JSON
Map<String, Object> result = objectMapper.readValue(responseBody, Map.class);
List<Map<String, Object>> detectionsJson = (List<Map<String, Object>>) result.get("detections");
List<Detection> detections = new ArrayList<>();
for (Map<String, Object> det : detectionsJson) {
String label = (String) det.get("label");
@@ -99,16 +111,16 @@ public class HttpYoloDetector implements YoloDetector {
int y = ((Number) det.get("y")).intValue();
int width = ((Number) det.get("width")).intValue();
int height = ((Number) det.get("height")).intValue();
detections.add(new Detection(label, confidence, new Rect(x, y, width, height), colorBGR));
}
return detections;
}
} catch (Exception e) {
log.error("HTTP检测请求失败: {}", e.getMessage());
}
return Collections.emptyList();
}
@@ -116,57 +128,57 @@ public class HttpYoloDetector implements YoloDetector {
private static class CustomByteArrayResource implements org.springframework.core.io.Resource {
private final byte[] byteArray;
private final String filename;
public CustomByteArrayResource(byte[] byteArray, String filename) {
this.byteArray = byteArray;
this.filename = filename;
}
@Override
public String getFilename() {
return this.filename;
}
@Override
public java.io.InputStream getInputStream() throws IOException {
return new java.io.ByteArrayInputStream(this.byteArray);
}
@Override
public boolean exists() {
return true;
}
@Override
public java.net.URL getURL() throws IOException {
throw new IOException("Not supported");
}
@Override
public java.net.URI getURI() throws IOException {
throw new IOException("Not supported");
}
@Override
public java.io.File getFile() throws IOException {
throw new IOException("Not supported");
}
@Override
public long contentLength() {
return this.byteArray.length;
}
@Override
public long lastModified() {
return System.currentTimeMillis();
}
@Override
public org.springframework.core.io.Resource createRelative(String relativePath) throws IOException {
throw new IOException("Not supported");
}
@Override
public String getDescription() {
return "Byte array resource [" + this.filename + "]";

View File

@@ -1,4 +1,16 @@
[
{"name":"smoke","path":"libs/models/smoke","size":[640,640],"backend":"opencv"},
{"name":"garbage","path":"libs/models/garbage","size":[640,640],"backend":"opencv"}
{
"name": "smoke",
"pythonModelName": "smoke",
"pythonApiUrl": "http://localhost:8000/api/detect/file",
"size": [640, 640],
"backend": "python"
},
{
"name": "garbage",
"pythonModelName": "garbage",
"pythonApiUrl": "http://localhost:8000/api/detect/file",
"size": [640, 640],
"backend": "python"
}
]

View File

@@ -6,6 +6,7 @@
<resultMap type="Device" id="DeviceResult">
<result property="deviceId" column="device_id" />
<result property="deviceName" column="device_name" />
<result property="ip" column="ip" />
<result property="type" column="type" />
<result property="userName" column="user_name" />
@@ -23,12 +24,13 @@
</resultMap>
<sql id="selectDeviceVo">
select device_id, ip, type, user_name, password, url, mediaKey, enabledFlv, enabledHls, mode, create_by, create_time, update_by, update_time, remark from v_device
select device_id, device_name, ip, type, user_name, password, url, mediaKey, enabledFlv, enabledHls, mode, create_by, create_time, update_by, update_time, remark from v_device
</sql>
<select id="selectDeviceList" parameterType="Device" resultMap="DeviceResult">
<include refid="selectDeviceVo"/>
<where>
<if test="deviceName != null and deviceName != ''"> and device_name like concat('%', #{deviceName}, '%')</if>
<if test="ip != null and ip != ''"> and ip = #{ip}</if>
<if test="type != null and type != ''"> and type = #{type}</if>
<if test="userName != null and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
@@ -49,6 +51,7 @@
<insert id="insertDevice" parameterType="Device" useGeneratedKeys="true" keyProperty="deviceId">
insert into v_device
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="deviceName != null and deviceName != ''">device_name,</if>
<if test="ip != null and ip != ''">ip,</if>
<if test="type != null and type != ''">type,</if>
<if test="userName != null and userName != ''">user_name,</if>
@@ -65,6 +68,7 @@
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="deviceName != null and deviceName != ''">#{deviceName},</if>
<if test="ip != null and ip != ''">#{ip},</if>
<if test="type != null and type != ''">#{type},</if>
<if test="userName != null and userName != ''">#{userName},</if>
@@ -85,6 +89,7 @@
<update id="updateDevice" parameterType="Device">
update v_device
<trim prefix="SET" suffixOverrides=",">
<if test="deviceName != null and deviceName != ''">device_name = #{deviceName},</if>
<if test="ip != null and ip != ''">ip = #{ip},</if>
<if test="type != null and type != ''">type = #{type},</if>
<if test="userName != null and userName != ''">user_name = #{userName},</if>

View File

@@ -1,14 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.video.mapper.InspectionTaskMapper">
<resultMap type="InspectionTask" id="InspectionTaskResult">
<result property="taskId" column="task_id" />
<result property="taskName" column="task_name" />
<result property="deviceId" column="device_id" />
<result property="deviceName" column="device_name" />
<result property="deviceIp" column="device_ip" />
<result property="cronExpression" column="cron_expression" />
<result property="duration" column="duration" />
<result property="threshold" column="threshold" />
@@ -17,6 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="executeCount" column="execute_count" />
<result property="alarmCount" column="alarm_count" />
<result property="lastExecuteTime" column="last_execute_time" />
<result property="modelName" column="model_name" />
<result property="remark" column="remark" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
@@ -25,24 +27,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectInspectionTaskVo">
select task_id, task_name, device_id, device_name, cron_expression, duration,
threshold, enable_detection, status, execute_count, alarm_count,
last_execute_time, remark, create_by, create_time, update_by, update_time
from v_inspection_task
select
t.task_id, t.task_name, t.device_id,
t.cron_expression, t.duration,
t.threshold, t.enable_detection, t.status, t.execute_count, t.alarm_count,
t.last_execute_time, t.model_name, t.remark, t.create_by, t.create_time, t.update_by, t.update_time,
d.device_name as device_name,
d.ip as device_ip
from v_inspection_task t
left join v_device d on t.device_id = d.device_id
</sql>
<select id="selectInspectionTaskList" parameterType="InspectionTask" resultMap="InspectionTaskResult">
<include refid="selectInspectionTaskVo"/>
<where>
<where>
<if test="taskName != null and taskName != ''"> and task_name like concat('%', #{taskName}, '%')</if>
<if test="deviceId != null"> and device_id = #{deviceId}</if>
<if test="deviceName != null and deviceName != ''"> and device_name like concat('%', #{deviceName}, '%')</if>
<if test="deviceName != null and deviceName != ''"> and d.device_name like concat('%', #{deviceName}, '%')</if>
<if test="status != null and status != ''"> and status = #{status}</if>
<if test="enableDetection != null and enableDetection != ''"> and enable_detection = #{enableDetection}</if>
<if test="modelName != null and modelName != ''"> and model_name = #{modelName}</if>
</where>
order by create_time desc
</select>
<select id="selectEnabledInspectionTaskList" resultMap="InspectionTaskResult">
<include refid="selectInspectionTaskVo"/>
where status = '0'
@@ -59,7 +67,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="taskName != null and taskName != ''">task_name,</if>
<if test="deviceId != null">device_id,</if>
<if test="deviceName != null">device_name,</if>
<if test="cronExpression != null and cronExpression != ''">cron_expression,</if>
<if test="duration != null">duration,</if>
<if test="threshold != null">threshold,</if>
@@ -68,6 +75,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="executeCount != null">execute_count,</if>
<if test="alarmCount != null">alarm_count,</if>
<if test="lastExecuteTime != null">last_execute_time,</if>
<if test="modelName != null and modelName != ''">model_name,</if>
<if test="remark != null">remark,</if>
<if test="createBy != null">create_by,</if>
create_time
@@ -75,7 +83,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="taskName != null and taskName != ''">#{taskName},</if>
<if test="deviceId != null">#{deviceId},</if>
<if test="deviceName != null">#{deviceName},</if>
<if test="cronExpression != null and cronExpression != ''">#{cronExpression},</if>
<if test="duration != null">#{duration},</if>
<if test="threshold != null">#{threshold},</if>
@@ -84,6 +91,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="executeCount != null">#{executeCount},</if>
<if test="alarmCount != null">#{alarmCount},</if>
<if test="lastExecuteTime != null">#{lastExecuteTime},</if>
<if test="modelName != null and modelName != ''">#{modelName},</if>
<if test="remark != null">#{remark},</if>
<if test="createBy != null">#{createBy},</if>
sysdate()
@@ -95,7 +103,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<trim prefix="SET" suffixOverrides=",">
<if test="taskName != null and taskName != ''">task_name = #{taskName},</if>
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="deviceName != null">device_name = #{deviceName},</if>
<if test="cronExpression != null and cronExpression != ''">cron_expression = #{cronExpression},</if>
<if test="duration != null">duration = #{duration},</if>
<if test="threshold != null">threshold = #{threshold},</if>
@@ -104,6 +111,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="executeCount != null">execute_count = #{executeCount},</if>
<if test="alarmCount != null">alarm_count = #{alarmCount},</if>
<if test="lastExecuteTime != null">last_execute_time = #{lastExecuteTime},</if>
<if test="modelName != null and modelName != ''">model_name = #{modelName},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = sysdate()
@@ -112,7 +120,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</update>
<update id="updateTaskExecuteInfo">
update v_inspection_task
update v_inspection_task
set execute_count = #{executeCount},
alarm_count = #{alarmCount},
last_execute_time = sysdate(),
@@ -125,7 +133,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</delete>
<delete id="deleteInspectionTaskByIds" parameterType="String">
delete from v_inspection_task where task_id in
delete from v_inspection_task where task_id in
<foreach item="taskId" collection="array" open="(" separator="," close=")">
#{taskId}
</foreach>