Compare commits
16 Commits
c32385e87d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f064c2e3e | ||
| 98c2aeaa9b | |||
| 98042b237c | |||
|
|
bf0996d750 | ||
|
|
3abac5ff1b | ||
| 99a8a943bc | |||
| f0b4c5a8bf | |||
|
|
f40d6ffcb6 | ||
|
|
524c8343e6 | ||
| aa32f9e9ac | |||
| 5f6058c024 | |||
| e3701991ef | |||
| 7096359434 | |||
|
|
4cec966613 | ||
| 1a7ecafc7d | |||
| 735704d585 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
18
python-inference-service/models.json
Normal file
18
python-inference-service/models.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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("垃圾识别模型已关闭")
|
||||
@@ -1,8 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "yolov8_detector",
|
||||
"path": "models/yolov8_model.py",
|
||||
"size": [640, 640],
|
||||
"comment": "YOLOv8检测模型,确保将训练好的best.pt文件放在models目录下"
|
||||
}
|
||||
]
|
||||
BIN
python-inference-service/models/smoke.pt
Normal file
BIN
python-inference-service/models/smoke.pt
Normal file
Binary file not shown.
1
python-inference-service/models/smoke.txt
Normal file
1
python-inference-service/models/smoke.txt
Normal file
@@ -0,0 +1 @@
|
||||
垃圾
|
||||
@@ -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
|
||||
@@ -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}模型已关闭")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
BIN
rtsp-vue/src/assets/images/camera.png
Normal file
BIN
rtsp-vue/src/assets/images/camera.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
BIN
rtsp-vue/src/assets/images/device.png
Normal file
BIN
rtsp-vue/src/assets/images/device.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
@@ -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" />
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
323
rtsp-vue/src/views/video/model/dict.vue
Normal file
323
rtsp-vue/src/views/video/model/dict.vue
Normal 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>
|
||||
@@ -109,6 +109,10 @@
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-framework</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-quartz</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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 + "]";
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user