AI Abandoned Object Detection System Development

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
AI Abandoned Object Detection System Development
Medium
~1-2 weeks
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_logo-aider_0.jpg
    AIDER company logo development
    762
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    848

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 недель