Files
jc-video-recognize/apps/server/services/vehicle_detection_service.py
T

348 lines
13 KiB
Python

"""
车辆检测服务适配器
支持车辆检测、跟踪和违停检测功能(基于YOLOv8)
"""
import cv2
import numpy as np
import logging
import threading
import time
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass
class VehicleTrackingInfo:
"""车辆跟踪信息"""
track_id: int
bbox: List[float]
center: Tuple[float, float]
first_seen: float
last_seen: float
plate_number: Optional[str] = None
is_illegal_parking: bool = False
trajectory: List[Tuple[float, float]] = None
def __post_init__(self):
if self.trajectory is None:
self.trajectory = []
class VehicleDetectionService:
"""车辆检测服务(YOLOv8模式)"""
# COCO数据集中车辆相关类别映射
VEHICLE_CLASSES = {2: 'car', 3: 'motorcycle', 5: 'bus', 7: 'truck'}
VEHICLE_LABELS = {'car': '小汽车', 'motorcycle': '摩托车', 'bus': '公交车', 'truck': '卡车'}
def __init__(self, yolo_model=None):
self.model_name = "vehicle_detection"
self.threshold = 0.1
self._lock = threading.Lock()
self._yolo_model = yolo_model
# 车辆跟踪信息
self.vehicle_tracks: Dict[int, VehicleTrackingInfo] = {}
self.track_id_counter = 0
# 违停检测配置
self.illegal_parking_time = 5.0
self.illegal_parking_region = None
self.available = yolo_model is not None
if self.available:
logger.info("车辆检测服务初始化完成(YOLOv8模式)")
else:
logger.error("车辆检测服务初始化失败:未提供YOLO模型")
def detect_image(self, image: np.ndarray, threshold: float = None) -> Dict:
"""
检测图片中的车辆
Args:
image: OpenCV 图片 (BGR格式)
threshold: 置信度阈值
Returns:
检测结果字典
"""
if threshold is None:
threshold = self.threshold
if not self.available or self._yolo_model is None:
return {
'success': False,
'message': '车辆检测服务不可用',
'detections': [],
'stats': None
}
try:
with self._lock:
start_time = time.time()
# 使用YOLO模型推理
inference_start = time.time()
results = self._yolo_model(image, conf=threshold, verbose=False)
inference_time = time.time() - inference_start
# 解析检测结果,只保留车辆类别
detections = []
result = results[0]
if result.boxes is not None:
for box in result.boxes:
cls_id = int(box.cls[0])
if cls_id in self.VEHICLE_CLASSES:
confidence = float(box.conf[0])
x1, y1, x2, y2 = box.xyxy[0].tolist()
center_x = (x1 + x2) / 2
center_y = (y1 + y2) / 2
class_name = self.VEHICLE_CLASSES[cls_id]
label = self.VEHICLE_LABELS[class_name]
detections.append({
'class': class_name,
'label': label,
'confidence': round(confidence, 3),
'bbox': [int(x1), int(y1), int(x2), int(y2)],
'center': [round(center_x, 2), round(center_y, 2)]
})
total_time = time.time() - start_time
return {
'success': True,
'message': '检测完成',
'detections': detections,
'stats': {
'total_detections': len(detections),
'model_used': 'yolov8s',
'threshold': threshold,
'processing_time': round(total_time, 3),
'inference_time': round(inference_time, 3)
}
}
except Exception as e:
import traceback
logger.error(f"检测失败: {e}")
logger.error(f"错误堆栈: {traceback.format_exc()}")
return {
'success': False,
'message': f'检测失败: {e}',
'detections': [],
'stats': None
}
def detect_illegal_parking(self, image: np.ndarray, threshold: float = None,
illegal_parking_time: float = 5.0,
region_polygon: List[Tuple[int, int]] = None) -> Dict:
"""
检测违停车辆
Args:
image: OpenCV 图片
threshold: 置信度阈值
illegal_parking_time: 违停时间阈值(秒)
region_polygon: 违停区域多边形点集 [(x1,y1), (x2,y2), ...]
Returns:
违停检测结果
"""
if threshold is None:
threshold = self.threshold
# 规范化 region_polygon 格式(支持 [{x,y},...] 和 [[x,y],...] 两种格式)
if region_polygon:
normalized = []
for p in region_polygon:
if isinstance(p, dict):
normalized.append((int(p['x']), int(p['y'])))
elif isinstance(p, (list, tuple)) and len(p) >= 2:
normalized.append((int(p[0]), int(p[1])))
region_polygon = normalized
# 更新违停配置
self.illegal_parking_time = illegal_parking_time
self.illegal_parking_region = region_polygon
# 基础车辆检测
detection_result = self.detect_image(image, threshold)
if not detection_result['success']:
return {
'success': False,
'message': detection_result['message'],
'illegal_parking': [],
'vehicles': []
}
current_time = time.time()
current_detections = detection_result['detections']
# 更新车辆跟踪信息
illegal_parking_vehicles = []
for detection in current_detections:
bbox = detection['bbox']
center = detection['center']
# 简单的跟踪(基于位置匹配)
matched_track_id = self._match_vehicle_to_track(center, bbox)
if matched_track_id is None:
# 新车辆
self.track_id_counter += 1
matched_track_id = self.track_id_counter
self.vehicle_tracks[matched_track_id] = VehicleTrackingInfo(
track_id=matched_track_id,
bbox=bbox,
center=center,
first_seen=current_time,
last_seen=current_time,
trajectory=[center]
)
else:
# 更新现有车辆
track_info = self.vehicle_tracks[matched_track_id]
track_info.bbox = bbox
track_info.center = center
track_info.last_seen = current_time
track_info.trajectory.append(center)
# 检查违停条件(对新车辆和已有车辆都检查)
track_info = self.vehicle_tracks[matched_track_id]
if self._check_illegal_parking(track_info, region_polygon):
track_info.is_illegal_parking = True
illegal_parking_vehicles.append({
'track_id': matched_track_id,
'bbox': bbox,
'center': center,
'parking_duration': round(current_time - track_info.first_seen, 2),
'plate_number': track_info.plate_number
})
# 清理长时间未出现的车辆
self._cleanup_old_tracks(current_time)
return {
'success': True,
'message': '违停检测完成',
'illegal_parking': illegal_parking_vehicles,
'total_vehicles': len(current_detections),
'stats': detection_result['stats']
}
def _match_vehicle_to_track(self, center: Tuple[float, float],
bbox: List[float]) -> Optional[int]:
"""将检测到的车辆匹配到已有轨迹"""
x, y = center
for track_id, track_info in self.vehicle_tracks.items():
track_x, track_y = track_info.center
# 计算距离
distance = np.sqrt((x - track_x) ** 2 + (y - track_y) ** 2)
# 距离阈值(基于检测框大小)
bbox_width = bbox[2] - bbox[0]
bbox_height = bbox[3] - bbox[1]
max_dim = max(bbox_width, bbox_height)
if distance < max_dim * 0.5: # 距离小于检测框最大尺寸的一半
return track_id
return None
def _check_illegal_parking(self, track_info: VehicleTrackingInfo,
region_polygon: List[Tuple[int, int]] = None) -> bool:
"""检查是否违停"""
current_time = time.time()
parking_duration = current_time - track_info.first_seen
# 检查时间是否超过阈值
if parking_duration < self.illegal_parking_time:
return False
# 如果指定了违停区域,检查车辆中心是否在多边形内
if region_polygon is not None:
return self._point_in_polygon(track_info.center, region_polygon)
# 未指定区域时,无法判定是否违停(避免全量误报)
return False
def _point_in_polygon(self, point: Tuple[float, float],
polygon: List[Tuple[int, int]]) -> bool:
"""判断点是否在多边形内(射线法)"""
x, y = point
n = len(polygon)
inside = False
p1x, p1y = polygon[0]
for i in range(n + 1):
p2x, p2y = polygon[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
def _cleanup_old_tracks(self, current_time: float):
"""清理长时间未出现的车辆轨迹"""
timeout = 10.0 # 10秒未出现则删除
tracks_to_remove = []
for track_id, track_info in self.vehicle_tracks.items():
if current_time - track_info.last_seen > timeout:
tracks_to_remove.append(track_id)
for track_id in tracks_to_remove:
del self.vehicle_tracks[track_id]
logger.debug(f"清理车辆轨迹: {track_id}")
def get_performance_info(self) -> Dict:
"""获取性能信息"""
return {
'mode': 'local',
'environment': 'YOLOv8',
'detector_loaded': self._yolo_model is not None,
'available': self.available,
'active_tracks': len(self.vehicle_tracks)
}
# 兼容性包装,保持与 YOLO 模型相同的接口
class VehicleDetectionModel:
"""车辆检测模型包装器,兼容 YOLO 接口"""
def __init__(self, yolo_model=None):
self._yolo_model = yolo_model
self.service = VehicleDetectionService(yolo_model=yolo_model)
# 保留 names 以兼容 YOLO 接口(实际类别由底层YOLO模型提供)
self.names = {0: 'vehicle'}
def __call__(self, image, conf=0.1, iou=0.45, verbose=False):
"""
直接委托给底层YOLO模型,返回标准YOLO结果
"""
if self._yolo_model is not None:
return self._yolo_model(image, conf=conf, iou=iou, verbose=verbose)
raise RuntimeError("YOLO模型未初始化")
def detect_illegal_parking(self, image, conf=0.1, illegal_parking_time=5.0,
region_polygon=None):
"""违停检测接口"""
return self.service.detect_illegal_parking(
image, conf, illegal_parking_time, region_polygon
)