Виявлення залишених предметів: від motion detection до ownership tracking
Залишений предмет — сумка біля колони в метро, коробка біля стійки реєстрації, рюкзак під сидінням автобуса. Завдання виглядає нескладним, поки не стикаєшся з реальним потоком: тисячі кадрів на годину, де «залишеним» може виявитися тінь, статичне сміття або людина, яка на хвилину присіла поруч із річчю.
Ми, команда AI-інженерів, спеціалізуємося на розробці таких систем під ключ. Наш досвід включає проєкти для вокзалів, аеропортів і торгових центрів. Гарантуємо recall > 90% при False Alarm Rate < 3 на годину на камеру після калібрування на вашому об'єкті. Зв'яжіться з нами для попередньої оцінки вашого проєкту — ми проаналізуємо вимоги та запропонуємо архітектуру.
Чому класичний motion detection не працює
MOG2 і KNN background subtractors виявляють зміни фону, а не факт залишення. Вони дають FAR 50–100 подій на годину на жвавій точці — охорона перестає реагувати через день експлуатації.
Реальне завдання — не детектувати статичний об'єкт, а встановити причинно-наслідковий зв'язок: об'єкт був при людині, людина пішла, об'єкт залишився.
Як ownership tracking вирішує проблему?
Замість порогів за статичністю ми вводимо поняття власника. Для кожного сумкоподібного об'єкта (backpack, handbag, suitcase) відстежуємо, чи була поруч людина. Якщо власник віддаляється і об'єкт залишається нерухомим задане число кадрів — система генерує тривогу. Ключовий компонент — ownership_distance (у пікселях, за замовчуванням 150 px). Архітектура з ownership hysteresis на 40% ефективніша за наївний трекінг за метрикою F1-score.
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
Чому ownership hysteresis — ключовий компонент?
Одне з головних джерел хибних тривог — ситуація, коли людина поставила сумку, відійшла на 2 метри взяти каву, і система вже вважає предмет покинутим. Рішення — ownership hysteresis: зв'язок «власник-предмет» розривається тільки якщо відстань перевищує поріг протягом N кадрів поспіль, а не в один момент.
Другий складний випадок: кілька людей стоять поруч із предметом, потім усі йдуть. Потрібно трекати last_owner_id з історією останніх 3–5 «власників».
Що таке ownership_distance і як його налаштувати?
ownership_distance (за замовчуванням 150 px) — максимальна евклідова відстань між центром сумки і центром найближчої людини, при якій людина вважається власником. Для великих просторів (аеропорт, стадіон) значення збільшують до 200–250 px, для вузьких коридорів — зменшують до 100 px. Підбір виконується на трейсі з ручною розміткою залишень.
Налаштування порогів для різних сценаріїв
| Сценарій | 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. Економія на FTE охорони становить до 70% за рахунок автоматизації. Окупність проекту — 8–12 місяців.
Що входить у розробку системи виявлення залишених предметів
Кожен проект включає:
- Навчену модель YOLOv8m, адаптовану під типи багажу на вашому об'єкті.
- Зібраний набір даних (не менше 1000 розмічених кадрів) з анотаціями.
- Конфігурацію ownership tracking з параметрами під ваш сценарій.
- REST API для інтеграції з VMS та системами оповіщення.
- Документацію з API та керівництво оператора.
- 30-денну гарантійну підтримку після запуску.
Етапи впровадження
- Аудит об'єкта та збір репрезентативної вибірки відео.
- Донавчання моделі (YOLOv8m/v8l) з урахуванням специфічних класів багажу.
- Налаштування ownership tracking під сценарій (static_frames, unattended_frames, owner_dist_px).
- Інтеграція з VMS (Milestone, Genetec, Trassir) через RTSP та webhook.
- Надання API для сповіщень (Telegram, e-mail, власна система).
- Гарантійна підтримка 1 місяць після запуску.
Строки розробки системи виявлення залишених предметів
| Масштаб | Строк |
|---|---|
| 1–4 камери, пілот | 3–4 тижні |
| 10–30 камер, production | 6–10 тижнів |
| 50+ камер, enterprise | 14–20 тижнів |
Оцінимо ваш проект і запропонуємо рішення. Отримайте консультацію — напишіть нам, обговоримо деталі. Замовте пілотний проект на 1–4 камери, щоб перевірити ефективність системи на вашому об'єкті.







