System Development распознавания нежелательного поведения (Behavior Detection)
Детекция нежелательного поведения — задача на стыке object detection, pose estimation и action recognition. Система должна понять не только «что находится в кадре», но и «что происходит»: драка, нападение, преследование, вандализм, попытка кражи. Сложность: граница между нормальным и нежелательным поведением размытая, и высокий recall требует работы с неизбежными ложными срабатываниями.
Иерархия обнаруживаемых событий
Уровень 1 (rule-based): простые события, определяемые правилами по позиции и движению объектов — не требуют ML. Быстро, низкое CPU.
Уровень 2 (pose-based): события на основе скелета человека. MediaPipe / RTMPose + анализ углов суставов и скорости движения.
Уровень 3 (video-based): глубокое видео-понимание через 3D CNN / Video Transformer. Высокая точность, требует GPU.
Детекция драки/агрессии
import torch
import torch.nn as nn
from torchvision.models.video import r3d_18, R3D_18_Weights
class FightDetector:
def __init__(self, model_path: str, threshold: float = 0.7):
base = r3d_18(weights=R3D_18_Weights.KINETICS400_V1)
base.fc = nn.Sequential(
nn.Linear(512, 128),
nn.GELU(),
nn.Dropout(0.4),
nn.Linear(128, 2) # fight / no_fight
)
base.load_state_dict(torch.load(model_path))
base.eval()
self.model = base
self.threshold = threshold
# Скользящее окно видео
self.frame_buffer = []
self.window_size = 16 # 16 кадров = ~0.5 сек при 30fps
def update(self, frame: np.ndarray) -> dict | None:
"""Обновление буфера и получение результата"""
self.frame_buffer.append(frame)
if len(self.frame_buffer) > self.window_size:
self.frame_buffer.pop(0)
if len(self.frame_buffer) == self.window_size:
return self._classify_window()
return None
@torch.no_grad()
def _classify_window(self) -> dict:
# [T, H, W, C] → [1, C, T, H, W]
clip = np.stack(self.frame_buffer)
clip = torch.from_numpy(clip).float().permute(3, 0, 1, 2)
clip = self._normalize(clip).unsqueeze(0)
logits = self.model(clip)
probs = torch.softmax(logits, dim=1).squeeze()
fight_prob = float(probs[1])
return {
'fight_detected': fight_prob > self.threshold,
'confidence': fight_prob
}
Skeleton-based Behavior Analysis
import numpy as np
from collections import deque
class BehaviorAnalyzer:
def __init__(self, window_size: int = 30):
self.track_history = {} # track_id -> deque of (frame, keypoints)
self.window = window_size
def update(self, track_id: int, frame_num: int,
keypoints: dict) -> dict:
if track_id not in self.track_history:
self.track_history[track_id] = deque(maxlen=self.window)
self.track_history[track_id].append((frame_num, keypoints))
if len(self.track_history[track_id]) < 10:
return {'behavior': 'unknown'}
return self._analyze(track_id)
def _analyze(self, track_id: int) -> dict:
history = list(self.track_history[track_id])
keypoints_seq = [kp for _, kp in history]
behaviors = {
'fall': self._detect_fall(keypoints_seq),
'running': self._detect_running(keypoints_seq),
'crouching': self._detect_crouching(keypoints_seq[-1]),
'loitering': self._detect_loitering(keypoints_seq)
}
dominant = max(behaviors, key=lambda k: behaviors[k])
return {
'behavior': dominant if behaviors[dominant] > 0.5 else 'normal',
'scores': behaviors
}
def _detect_fall(self, seq: list) -> float:
"""Детекция падения: резкое снижение центра масс"""
hip_ys = []
for kp in seq:
if kp.get('left_hip') and kp.get('right_hip'):
avg_hip_y = (kp['left_hip']['y'] + kp['right_hip']['y']) / 2
hip_ys.append(avg_hip_y)
if len(hip_ys) < 10:
return 0.0
# Нормализованные координаты: y растёт вниз
max_drop = max(hip_ys[-5:]) - min(hip_ys[-15:-5]) if len(hip_ys) >= 15 else 0
return min(1.0, max_drop / 0.3) # 0.3 = 30% высоты кадра
def _detect_loitering(self, seq: list) -> float:
"""Детекция задержки: человек долго на одном месте"""
if len(seq) < 20:
return 0.0
positions = [(kp.get('nose', {}).get('x', 0.5),
kp.get('nose', {}).get('y', 0.5))
for kp in seq]
positions = np.array(positions)
spread = np.std(positions, axis=0).mean()
return min(1.0, (0.05 - spread) / 0.05) # < 5% spread = loitering
Ложные срабатывания и коррекция
Главная проблема агрессивных детекторов — false positives. Подход:
- Temporal confirmation: событие подтверждается только если детектируется N кадров подряд
- Multi-evidence fusion: комбинирование skeleton + video + context (время суток, зона)
- Human-in-the-loop: подозрительные события отправляются на проверку охраннику, подтверждённые используются для дообучения
| Тип поведения | Precision | Recall |
|---|---|---|
| Падение | 91% | 94% |
| Бег/спешка | 88% | 92% |
| Агрессия/драка | 82% | 88% |
| Вандализм | 79% | 83% |
| Кража в кармане | 74% | 79% |
| Масштаб | Срок |
|---|---|
| 2–3 типа событий, rule-based + pose | 4–6 недель |
| Полная аналитика поведения | 9–14 недель |
| Высокоточная система с обучением | 14–22 недели |







