System Development обнаружения оставленных предметов
Оставленный предмет — сумка у колонны в метро, коробка у стойки регистрации, рюкзак под сиденьем автобуса. Задача выглядит несложной, пока не сталкиваешься с реальным потоком: тысячи кадров в час, где «оставленным» может оказаться тень, статичный мусор, или человек, который на минуту присел рядом с вещью.
Хорошая система оставленных предметов должна держать recall > 90% при False Alarm Rate < 3 в час на камеру в условиях загруженного общественного пространства.
Почему классический motion detection здесь не работает
MOG2 и KNN background subtractors обнаруживают изменения фона, а не факт оставления. Они дают FAR 50–100 событий в час на оживлённой точке — охрана перестаёт реагировать через день эксплуатации.
Реальная задача — не «детектировать статичный объект», а установить причинно-следственную связь: объект был при человеке, человек ушёл, объект остался.
Architecture детектора оставленных предметов
import cv2
import numpy as np
from ultralytics import YOLO
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class TrackedObject:
obj_id: int
bbox: list
class_name: str
last_owner_id: Optional[int] # track_id человека-владельца
frames_static: int = 0
frames_unattended: int = 0
is_abandoned: bool = False
class AbandonedObjectDetector:
def __init__(self, model_path: str, config: dict):
self.detector = YOLO(model_path)
self.objects: dict[int, TrackedObject] = {}
self.persons: dict = {}
# Ключевые пороги — именно здесь вся магия
self.static_threshold = config.get('static_frames', 90) # 3 сек @ 30fps
self.unattended_threshold = config.get('unattended_frames', 150) # 5 сек
self.ownership_distance = config.get('owner_dist_px', 150) # пикселей
self.bag_classes = ['backpack', 'handbag', 'suitcase',
'umbrella', 'sports ball']
def _find_owner(self, obj_bbox: list,
person_tracks: list) -> Optional[int]:
"""Ищем ближайшего человека в радиусе ownership_distance"""
obj_center = np.array([(obj_bbox[0]+obj_bbox[2])//2,
(obj_bbox[1]+obj_bbox[3])//2])
min_dist = float('inf')
owner_id = None
for person in person_tracks:
p_center = np.array([(person.bbox[0]+person.bbox[2])//2,
(person.bbox[1]+person.bbox[3])//2])
dist = np.linalg.norm(obj_center - p_center)
if dist < min_dist and dist < self.ownership_distance:
min_dist = dist
owner_id = person.track_id
return owner_id
def process_frame(self, frame: np.ndarray) -> list[TrackedObject]:
results = self.detector.track(frame, persist=True,
classes=[0,24,26,28]) # person+bags
abandoned = []
persons = [r for r in results[0].boxes
if self.detector.model.names[int(r.cls)] == 'person']
bags = [r for r in results[0].boxes
if self.detector.model.names[int(r.cls)] in self.bag_classes]
for bag in bags:
bid = int(bag.id) if bag.id is not None else -1
bbox = list(map(int, bag.xyxy[0]))
if bid not in self.objects:
self.objects[bid] = TrackedObject(
obj_id=bid,
bbox=bbox,
class_name=self.detector.model.names[int(bag.cls)],
last_owner_id=None
)
tracked = self.objects[bid]
owner = self._find_owner(bbox, persons)
if owner is not None:
tracked.last_owner_id = owner
tracked.frames_unattended = 0 # сброс счётчика
else:
if tracked.last_owner_id is not None:
tracked.frames_unattended += 1
# Статичность объекта
prev_center = np.array([(tracked.bbox[0]+tracked.bbox[2])//2,
(tracked.bbox[1]+tracked.bbox[3])//2])
curr_center = np.array([(bbox[0]+bbox[2])//2, (bbox[1]+bbox[3])//2])
if np.linalg.norm(curr_center - prev_center) < 5:
tracked.frames_static += 1
else:
tracked.frames_static = 0
tracked.bbox = bbox
if (tracked.frames_static >= self.static_threshold and
tracked.frames_unattended >= self.unattended_threshold):
tracked.is_abandoned = True
abandoned.append(tracked)
return abandoned
Проблема «временного хозяина»
Один из главных источников ложных тревог — ситуация, когда человек поставил сумку, отошёл на 2 метра взять кофе, и система уже считает предмет брошенным. Решение — ownership hysteresis: связь «владелец-предмет» разрывается только если расстояние превышает порог в течение N кадров подряд, а не в один момент.
Второй сложный случай: несколько людей стоят рядом с предметом, потом все уходят. Нужно трекать last_owner_id с историей последних 3–5 «хозяев».
Настройка под сценарий
| Сценарий | static_frames | unattended_frames | owner_dist_px |
|---|---|---|---|
| Метро, высокий трафик | 60 (2 сек) | 90 (3 сек) | 120 |
| Аэропорт, низкий трафик | 150 (5 сек) | 300 (10 сек) | 180 |
| Офисный холл | 300 (10 сек) | 600 (20 сек) | 200 |
| Склад/стоянка | 450 (15 сек) | 900 (30 сек) | 250 |
Кейс: вокзал, 12 камер
На одном вокзале запустили naive static object detection — 80+ ложных тревог в смену. Персонал жаловался и стал игнорировать систему. После внедрения ownership tracking с гистерезисом 3 секунды и минимальным размером bbox 40×40 пикселей (фильтр мусора на полу):
- FAR снизился с 80+ до 4–6 событий в смену
- Recall на тестовом наборе (30 инсценированных оставлений): 93%
- Среднее время до тревоги: 8 секунд после фактического оставления
Модель: YOLOv8m, дообученная на 2400 изображениях вокзальных сумок в разных ракурсах. Инференс на NVIDIA T4 — 28ms на кадр при 1080p.
Integration и деплой
- Видеопоток: RTSP через GStreamer или FFmpeg с буфером 200ms
- Хранение событий: снимок кадра + 30-секундный клип до/после в S3
- Уведомления: webhook → охрана, Telegram-бот, VMS-интеграция
- Edge деплой: NVIDIA Jetson Orin (NX 16GB) тянет 8 потоков 1080p@15fps
| Масштаб | Срок |
|---|---|
| 1–4 камеры, пилот | 3–4 недели |
| 10–30 камер, production | 6–10 недель |
| 50+ камер, enterprise | 14–20 недель |







