From 7aa71c5f83b6a1f22fc2bb273e41162482cec353 Mon Sep 17 00:00:00 2001 From: wwh <496479012@qq.com> Date: Tue, 19 May 2026 09:17:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BA=BA=E5=91=98?= =?UTF-8?q?=E5=BE=98=E5=BE=8A/=E9=9D=99=E6=AD=A2=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交实现了完整的人员行为分析系统,包括: 1. 新增基于位置和跟踪ID的两种行为检测算法 2. 新增徘徊检测服务与行为处理器模块 3. 前后端集成算法配置界面与告警展示 4. 支持图片和视频流场景下的行为分析 5. 新增算法配置接口与文档说明 具体改动: - 新增loitering_detection模型目录与算法实现 - 新增AlgorithmConfig组件实现可视化配置 - 扩展图片/视频检测接口支持算法参数传递 - 新增行为告警推送与前端展示页面 - 优化检测服务,集成行为分析逻辑 - 移除冗余日志输出,完善代码注释 --- apps/server/api/detection.py | 103 +++++- apps/server/services/camera_service.py | 14 +- apps/server/services/detection_service.py | 251 ++++++++++---- apps/server/services/loitering_service.py | 168 +++++++++ apps/server/services/model_service.py | 1 - apps/web/src/api/detection.js | 16 +- apps/web/src/components/AlgorithmConfig.vue | 326 ++++++++++++++++++ apps/web/src/components/ImageDetection.vue | 118 ++++++- apps/web/src/components/VideoDetection.vue | 148 +++++++- models/README.md | 163 +++++++++ .../algorithms/__init__.py | 9 + .../algorithms/loitering_detector.py | 251 ++++++++++++++ .../algorithms/stationary_detector.py | 236 +++++++++++++ .../processors/__init__.py | 8 + .../processors/behavior_processor.py | 201 +++++++++++ 15 files changed, 1937 insertions(+), 76 deletions(-) create mode 100644 apps/server/services/loitering_service.py create mode 100644 apps/web/src/components/AlgorithmConfig.vue create mode 100644 models/loitering_detection/algorithms/__init__.py create mode 100644 models/loitering_detection/algorithms/loitering_detector.py create mode 100644 models/loitering_detection/algorithms/stationary_detector.py create mode 100644 models/loitering_detection/processors/__init__.py create mode 100644 models/loitering_detection/processors/behavior_processor.py diff --git a/apps/server/api/detection.py b/apps/server/api/detection.py index d986789..a79c7bd 100644 --- a/apps/server/api/detection.py +++ b/apps/server/api/detection.py @@ -2,24 +2,50 @@ import cv2 import numpy as np import base64 import logging +import json +from typing import Optional from fastapi import APIRouter, UploadFile, File, Form, Query from models.schemas import ImageDetectionResult router = APIRouter() logger = logging.getLogger(__name__) + @router.post("/detect/image", response_model=ImageDetectionResult) async def detect_image( file: UploadFile = File(...), model_id: str = Query("fire_detection"), confidence: float = Query(0.5), - iou: float = Query(0.45) + iou: float = Query(0.45), + algorithm_config: Optional[str] = Query(None, description="算法配置JSON字符串") ): + """ + 图片检测接口 + + Args: + algorithm_config: 算法配置JSON,例如: + { + "enable_stationary_detection": true, + "enable_loitering_detection": false, + "stationary_threshold": 10.0, + "position_tolerance": 50, + "loitering_threshold": 300.0, + "movement_threshold": 5.0 + } + """ from main import model_service from services.detection_service import DetectionService detection_service = DetectionService(model_service) + # 解析算法配置 + algo_config = None + if algorithm_config: + try: + algo_config = json.loads(algorithm_config) + except json.JSONDecodeError as e: + logger.warning(f"算法配置解析失败: {e}") + try: contents = await file.read() nparr = np.frombuffer(contents, np.uint8) @@ -32,10 +58,14 @@ async def detect_image( data={} ) - result = await detection_service.detect_image(frame, model_id, confidence, iou) + result = await detection_service.detect_image( + frame, model_id, confidence, iou, algorithm_config=algo_config + ) if result['success']: - annotated_frame = detection_service.draw_detections(frame, result['detections']) + annotated_frame = detection_service.draw_detections( + frame, result['detections'], algorithm_config=algo_config + ) # 将标注后的图片转换为 base64 _, buffer = cv2.imencode('.jpg', annotated_frame) @@ -47,7 +77,9 @@ async def detect_image( data={ "detections": result['detections'], "image_base64": img_base64, - "stats": result['stats'] + "stats": result['stats'], + "alerts": result.get('alerts', []), + "behavior_stats": result.get('behavior_stats', {}) } ) else: @@ -64,3 +96,66 @@ async def detect_image( message=f"检测失败: {str(e)}", data={} ) + + +@router.get("/algorithms/config") +async def get_algorithm_config(): + """获取算法配置选项""" + return { + "algorithms": [ + { + "id": "stationary_detection", + "name": "静止检测", + "description": "检测人员在同一位置静止停留", + "params": [ + { + "name": "stationary_threshold", + "label": "静止阈值", + "type": "number", + "default": 10.0, + "min": 1.0, + "max": 300.0, + "unit": "秒", + "description": "超过此时间视为静止" + }, + { + "name": "position_tolerance", + "label": "位置容差", + "type": "number", + "default": 50, + "min": 10, + "max": 200, + "unit": "像素", + "description": "位置匹配容差范围" + } + ] + }, + { + "id": "loitering_detection", + "name": "徘徊检测", + "description": "检测人员长时间停留(需要跟踪ID)", + "params": [ + { + "name": "loitering_threshold", + "label": "徘徊阈值", + "type": "number", + "default": 300.0, + "min": 60.0, + "max": 1800.0, + "unit": "秒", + "description": "超过此时间视为徘徊" + }, + { + "name": "movement_threshold", + "label": "移动阈值", + "type": "number", + "default": 5.0, + "min": 1.0, + "max": 50.0, + "unit": "像素", + "description": "小于此移动视为静止" + } + ] + } + ] + } diff --git a/apps/server/services/camera_service.py b/apps/server/services/camera_service.py index 5ddaecf..46d2783 100644 --- a/apps/server/services/camera_service.py +++ b/apps/server/services/camera_service.py @@ -249,11 +249,21 @@ class CameraService: logger.info(f"发送检测结果: {len(result['detections'])} 个目标, {result['stats']}") - await websocket.send_json({ + detection_message = { 'type': 'detection', 'detections': result['detections'], 'stats': result['stats'] - }) + } + + # 包含行为告警信息 + if 'alerts' in result and result['alerts']: + detection_message['alerts'] = result['alerts'] + logger.info(f"发送告警: {len(result['alerts'])} 个") + + if 'behavior_stats' in result: + detection_message['behavior_stats'] = result['behavior_stats'] + + await websocket.send_json(detection_message) _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80]) import base64 diff --git a/apps/server/services/detection_service.py b/apps/server/services/detection_service.py index 9aa0b9f..0f64e3e 100644 --- a/apps/server/services/detection_service.py +++ b/apps/server/services/detection_service.py @@ -7,6 +7,8 @@ import logging from typing import Dict, List, Optional from PIL import Image, ImageDraw, ImageFont +from .loitering_service import get_loitering_service + logger = logging.getLogger(__name__) class DetectionService: @@ -18,64 +20,20 @@ class DetectionService: os.makedirs(self.results_dir, exist_ok=True) os.makedirs(self.temp_dir, exist_ok=True) - - def draw_detections(self, frame: np.ndarray, detections: List[Dict], fps: float = 0) -> np.ndarray: - try: - img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - pil_img = Image.fromarray(img_rgb) - draw = ImageDraw.Draw(pil_img) - - try: - font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 20) - font_large = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 24) - except: - font = ImageFont.load_default() - font_large = font - - class_colors = { - 'Fire': (255, 0, 0), - 'Smoke': (128, 128, 128), - 'person': (0, 255, 0), - 'helmet': (255, 255, 0), - 'no_helmet': (255, 0, 255), - 'cigarette': (0, 165, 255) # 橙色,用于抽烟检测 - } - - for det in detections: - x1, y1, x2, y2 = det['bbox'] - class_name = det['class'] - conf = det['confidence'] - label = det['label'] - - color = class_colors.get(class_name, (0, 255, 0)) - - draw.rectangle([x1, y1, x2, y2], outline=color, width=3) - - label_text = f"{label} {conf:.2f}" - bbox = draw.textbbox((0, 0), label_text, font=font) - text_w = bbox[2] - bbox[0] - text_h = bbox[3] - bbox[1] - draw.rectangle([x1, y1 - text_h - 4, x1 + text_w + 4, y1], fill=color) - draw.text((x1 + 2, y1 - text_h - 2), label_text, fill=(255, 255, 255), font=font) - - if fps > 0: - fps_text = f"FPS: {fps:.1f} | Detections: {len(detections)}" - draw.text((10, 10), fps_text, fill=(0, 255, 0), font=font) - - return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) - except Exception as e: - logger.error(f"绘制检测结果失败: {e}") - return frame + + # 初始化徘徊检测服务(懒加载,实际初始化在第一次使用时) + self.loitering_service = get_loitering_service() async def detect_image( - self, + self, image: np.ndarray, model_id: str, confidence: float = 0.5, - iou: float = 0.45 + iou: float = 0.45, + algorithm_config: Optional[Dict] = None ) -> Dict: start_time = time.time() - + model = await self.model_service.load_model(model_id) if not model: return { @@ -84,10 +42,10 @@ class DetectionService: 'detections': [], 'stats': None } - + try: results = model(image, conf=confidence, iou=iou, verbose=False) - + detections = [] for result in results: boxes = result.boxes @@ -96,21 +54,21 @@ class DetectionService: conf = float(box.conf[0].cpu().numpy()) cls = int(box.cls[0].cpu().numpy()) class_name = result.names[cls] - + label_map = self.model_service.model_configs[model_id]['labels'] label = label_map.get(class_name, class_name) - + detections.append({ 'class': class_name, 'label': label, 'confidence': round(conf, 3), 'bbox': [int(x1), int(y1), int(x2), int(y2)] }) - + processing_time = time.time() - start_time avg_confidence = sum(d['confidence'] for d in detections) / len(detections) if detections else 0 - - return { + + result_data = { 'success': True, 'message': '检测完成', 'detections': detections, @@ -121,6 +79,14 @@ class DetectionService: 'model_used': model_id } } + + # 如果启用了行为检测算法 + if algorithm_config and detections: + result_data = self._apply_behavior_analysis( + result_data, algorithm_config + ) + + return result_data except Exception as e: logger.error(f"图片检测失败: {e}") return { @@ -186,9 +152,40 @@ class DetectionService: } } + # 如果是人员检测模型,进行行为分析 + logger.info(f"[DetectionService] 模型: {model_id}, 检测目标: {len(detections)}") + if model_id == 'loitering_detection' and detections: + logger.info("[DetectionService] 调用行为分析...") + + # 确保服务已初始化 + if not self.loitering_service.is_initialized: + logger.info("[DetectionService] 初始化徘徊检测服务...") + self.loitering_service.initialize( + # 检测阈值(用于判断是否静止/徘徊) + stationary_threshold=10.0, + position_tolerance=50, + loitering_threshold=300.0, + movement_threshold=5.0, + # 告警阈值(用于触发告警,应该比检测阈值高) + stationary_alert_threshold=30.0, + loitering_alert_threshold=600.0, + # 启用告警 + enable_stationary_alert=True, + enable_loitering_alert=True + ) + + behavior_result = self.loitering_service.process_detections( + detections, + use_tracking=False # 可以改为 True 如果使用跟踪 + ) + detections = behavior_result['detections'] + result_data['alerts'] = behavior_result['alerts'] + result_data['behavior_stats'] = behavior_result['stats'] + logger.info(f"[DetectionService] 行为分析完成: alerts={len(behavior_result['alerts'])}, stats={behavior_result['stats']}") + if draw: frame = self.draw_detections(frame, detections, fps) - + return frame, result_data except Exception as e: logger.error(f"帧检测失败: {e}") @@ -197,3 +194,139 @@ class DetectionService: 'detections': [], 'stats': None } + + def _apply_behavior_analysis( + self, + result_data: Dict, + algorithm_config: Dict + ) -> Dict: + """ + 应用行为分析算法 + + Args: + result_data: 检测结果 + algorithm_config: 算法配置 + { + "enable_stationary_detection": true, + "enable_loitering_detection": false, + "stationary_threshold": 10.0, + "position_tolerance": 50, + ... + } + + Returns: + 添加行为分析结果的检测结果 + """ + detections = result_data['detections'] + + # 检查是否需要行为分析 + enable_stationary = algorithm_config.get('enable_stationary_detection', False) + enable_loitering = algorithm_config.get('enable_loitering_detection', False) + + if not enable_stationary and not enable_loitering: + return result_data + + try: + # 使用前端传入的配置初始化服务 + self.loitering_service.initialize( + stationary_threshold=algorithm_config.get('stationary_threshold', 10.0), + position_tolerance=algorithm_config.get('position_tolerance', 50), + loitering_threshold=algorithm_config.get('loitering_threshold', 300.0), + movement_threshold=algorithm_config.get('movement_threshold', 5.0), + enable_stationary_alert=enable_stationary, + enable_loitering_alert=enable_loitering + ) + + # 处理检测 + behavior_result = self.loitering_service.process_detections( + detections, + use_tracking=enable_loitering # 只有启用徘徊检测时才使用跟踪 + ) + + result_data['detections'] = behavior_result['detections'] + result_data['alerts'] = behavior_result['alerts'] + result_data['behavior_stats'] = behavior_result['stats'] + + except Exception as e: + logger.error(f"行为分析失败: {e}") + result_data['behavior_error'] = str(e) + + return result_data + + def draw_detections( + self, + frame: np.ndarray, + detections: List[Dict], + fps: float = 0, + algorithm_config: Optional[Dict] = None + ) -> np.ndarray: + """ + 绘制检测结果和行为告警 + + Args: + frame: 图像帧 + detections: 检测结果列表(可能包含 stationary_info/loitering_info) + fps: 帧率 + algorithm_config: 算法配置(已废弃,保留用于向后兼容) + """ + try: + img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_img = Image.fromarray(img_rgb) + draw = ImageDraw.Draw(pil_img) + + try: + font = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 20) + font_large = ImageFont.truetype("/System/Library/Fonts/PingFang.ttc", 24) + except: + font = ImageFont.load_default() + font_large = font + + class_colors = { + 'Fire': (255, 0, 0), + 'Smoke': (128, 128, 128), + 'person': (0, 255, 0), + 'helmet': (255, 255, 0), + 'no_helmet': (255, 0, 255), + 'cigarette': (0, 165, 255) + } + + for det in detections: + x1, y1, x2, y2 = det['bbox'] + class_name = det['class'] + conf = det['confidence'] + label = det['label'] + + # 根据是否有行为告警选择颜色 + color = class_colors.get(class_name, (0, 255, 0)) + + # 检查行为告警 + if algorithm_config: + if 'stationary_info' in det: + info = det['stationary_info'] + if info.get('is_stationary'): + color = (0, 0, 255) # 红色警告 + label = f"静止{int(info['duration'])}s" + + if 'loitering_info' in det: + info = det['loitering_info'] + if info.get('is_loitering'): + color = (255, 0, 0) # 蓝色警告 + label = f"徘徊{int(info['loitering_duration']//60)}min" + + draw.rectangle([x1, y1, x2, y2], outline=color, width=3) + + label_text = f"{label} {conf:.2f}" + bbox = draw.textbbox((0, 0), label_text, font=font) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + draw.rectangle([x1, y1 - text_h - 4, x1 + text_w + 4, y1], fill=color) + draw.text((x1 + 2, y1 - text_h - 2), label_text, fill=(255, 255, 255), font=font) + + if fps > 0: + fps_text = f"FPS: {fps:.1f} | Detections: {len(detections)}" + draw.text((10, 10), fps_text, fill=(0, 255, 0), font=font) + + return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) + except Exception as e: + logger.error(f"绘制检测结果失败: {e}") + return frame diff --git a/apps/server/services/loitering_service.py b/apps/server/services/loitering_service.py new file mode 100644 index 0000000..c9c401b --- /dev/null +++ b/apps/server/services/loitering_service.py @@ -0,0 +1,168 @@ +""" +徘徊检测服务 +集成行为检测算法到后端服务 +""" + +import sys +import os +from typing import Dict, List, Optional +import logging + +# 添加算法模块路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'models', 'loitering_detection')) + +from processors import BehaviorProcessor + +logger = logging.getLogger(__name__) + + +class LoiteringService: + """ + 徘徊检测服务 + + 为视频流检测提供行为分析功能: + - 静止检测(基于位置,无需跟踪) + - 徘徊检测(基于跟踪ID) + """ + + def __init__(self): + self.processor = None + self.is_initialized = False + + def initialize( + self, + stationary_threshold: float = 10.0, + position_tolerance: int = 50, + loitering_threshold: float = 300.0, + movement_threshold: float = 5.0, + enable_stationary_alert: bool = True, + enable_loitering_alert: bool = True, + stationary_alert_threshold: Optional[float] = None, + loitering_alert_threshold: Optional[float] = None + ): + """ + 初始化服务 + + Args: + stationary_threshold: 静止检测阈值(秒)- 用于判断是否静止 + position_tolerance: 位置容差(像素) + loitering_threshold: 徘徊检测阈值(秒)- 用于判断是否徘徊 + movement_threshold: 移动阈值(像素) + enable_stationary_alert: 是否启用静止告警 + enable_loitering_alert: 是否启用徘徊告警 + stationary_alert_threshold: 静止告警阈值(秒)- 超过此时间产生告警,默认等于 stationary_threshold + loitering_alert_threshold: 徘徊告警阈值(秒)- 超过此时间产生告警,默认等于 loitering_threshold + """ + try: + self.processor = BehaviorProcessor( + stationary_threshold=stationary_threshold, + position_tolerance=position_tolerance, + loitering_threshold=loitering_threshold, + movement_threshold=movement_threshold, + enable_stationary_alert=enable_stationary_alert, + enable_loitering_alert=enable_loitering_alert, + stationary_alert_threshold=stationary_alert_threshold if stationary_alert_threshold is not None else stationary_threshold, + loitering_alert_threshold=loitering_alert_threshold if loitering_alert_threshold is not None else loitering_threshold + ) + self.is_initialized = True + logger.info(f"徘徊检测服务初始化成功: 静止阈值={stationary_threshold}s, 告警阈值={stationary_alert_threshold or stationary_threshold}s") + except Exception as e: + logger.error(f"徘徊检测服务初始化失败: {e}") + self.is_initialized = False + + def process_detections( + self, + detections: List[Dict], + use_tracking: bool = False, + track_id_key: str = 'track_id' + ) -> Dict: + """ + 处理检测结果 + + Args: + detections: YOLO检测结果列表 + use_tracking: 是否使用跟踪ID + track_id_key: 跟踪ID字段名 + + Returns: + { + 'detections': 添加行为信息的检测结果, + 'alerts': 触发的告警列表, + 'stats': 统计信息 + } + """ + if not self.is_initialized or not self.processor: + return { + 'detections': detections, + 'alerts': [], + 'stats': {'error': '服务未初始化'} + } + + try: + return self.processor.process( + detections=detections, + use_tracking=use_tracking, + track_id_key=track_id_key + ) + except Exception as e: + logger.error(f"处理检测结果失败: {e}") + return { + 'detections': detections, + 'alerts': [], + 'stats': {'error': str(e)} + } + + def get_stationary_persons(self) -> List[Dict]: + """获取所有静止人员""" + if not self.is_initialized or not self.processor: + return [] + return self.processor.get_stationary_persons() + + def get_loitering_persons(self) -> List[Dict]: + """获取所有徘徊人员""" + if not self.is_initialized or not self.processor: + return [] + return self.processor.get_loitering_persons() + + def reset(self): + """重置检测器""" + if self.processor: + self.processor.reset() + logger.info("徘徊检测器已重置") + + def get_config(self) -> Dict: + """获取当前配置""" + if not self.is_initialized or not self.processor: + return {'error': '服务未初始化'} + return self.processor.get_config() + + def get_stats(self) -> Dict: + """获取统计信息""" + if not self.is_initialized or not self.processor: + return {'error': '服务未初始化'} + + stats = { + 'stationary_count': len(self.get_stationary_persons()), + 'loitering_count': len(self.get_loitering_persons()), + 'config': self.get_config() + } + return stats + + +# 全局服务实例 +_loitering_service: Optional[LoiteringService] = None + + +def get_loitering_service() -> LoiteringService: + """获取全局徘徊检测服务实例""" + global _loitering_service + if _loitering_service is None: + _loitering_service = LoiteringService() + return _loitering_service + + +def initialize_loitering_service(**kwargs): + """初始化全局徘徊检测服务""" + service = get_loitering_service() + service.initialize(**kwargs) + return service diff --git a/apps/server/services/model_service.py b/apps/server/services/model_service.py index 0657600..644d7ef 100644 --- a/apps/server/services/model_service.py +++ b/apps/server/services/model_service.py @@ -82,7 +82,6 @@ class ModelService: return None if model_id in self.models: - logger.info(f"模型已加载: {model_id}") return self.models[model_id] config = self.model_configs[model_id] diff --git a/apps/web/src/api/detection.js b/apps/web/src/api/detection.js index cb36f76..b935bfb 100644 --- a/apps/web/src/api/detection.js +++ b/apps/web/src/api/detection.js @@ -9,12 +9,22 @@ export const detectionApi = { getModels() { return api.get('/models') }, - - detectImage(formData) { + + getAlgorithmConfig() { + return api.get('/algorithms/config') + }, + + detectImage(formData, algorithmConfig = null) { + const params = {} + if (algorithmConfig) { + params.algorithm_config = JSON.stringify(algorithmConfig) + } + return api.post('/detect/image', formData, { headers: { 'Content-Type': 'multipart/form-data' - } + }, + params }) } } diff --git a/apps/web/src/components/AlgorithmConfig.vue b/apps/web/src/components/AlgorithmConfig.vue new file mode 100644 index 0000000..583efef --- /dev/null +++ b/apps/web/src/components/AlgorithmConfig.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/apps/web/src/components/ImageDetection.vue b/apps/web/src/components/ImageDetection.vue index c97c118..7a124c5 100644 --- a/apps/web/src/components/ImageDetection.vue +++ b/apps/web/src/components/ImageDetection.vue @@ -93,6 +93,13 @@
{{ config.iou.toFixed(2) }}
+ + + @@ -225,6 +232,7 @@ import { QuestionFilled } from '@element-plus/icons-vue' import { detectionApi } from '@/api/detection' +import AlgorithmConfig from './AlgorithmConfig.vue' const props = defineProps({ models: { @@ -236,7 +244,8 @@ const props = defineProps({ const config = ref({ model: props.models.length > 0 ? props.models[0].id : 'fire_detection', confidence: 0.5, - iou: 0.45 + iou: 0.45, + algorithmConfig: {} }) // 可拖拽调整宽度相关 @@ -271,7 +280,20 @@ const originalImage = ref('') const resultImage = ref('') const detections = ref([]) const stats = ref(null) -const uploadUrl = computed(() => `/api/detect/image?model_id=${config.value.model}&confidence=${config.value.confidence}&iou=${config.value.iou}`) +const uploadUrl = computed(() => { + const params = new URLSearchParams({ + model_id: config.value.model, + confidence: config.value.confidence, + iou: config.value.iou + }) + + // 添加算法配置 + if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) { + params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig)) + } + + return `/api/detect/image?${params.toString()}` +}) const formatConfidence = (value) => { return `置信度: ${value.toFixed(2)}` @@ -303,6 +325,22 @@ const handleUploadSuccess = (response) => { } detections.value = response.data.detections || [] stats.value = response.data.stats + + // 处理告警信息 + if (response.data.alerts && response.data.alerts.length > 0) { + alerts.value = response.data.alerts + console.log('收到告警:', response.data.alerts) + + // 显示告警通知 + response.data.alerts.forEach(alert => { + ElMessage({ + message: `行为告警: ${alert.type} - ${alert.message}`, + type: 'warning', + duration: 3000 + }) + }) + } + ElMessage.success('检测完成') } else { ElMessage.error(response.message) @@ -320,6 +358,10 @@ const modelName = computed(() => { const model = props.models.find(m => m.id === config.value.model) return model ? model.name : config.value.model }) + +const onAlgorithmChange = (algoConfig) => { + config.value.algorithmConfig = algoConfig +}