Разработка AI-системы мониторинга периметра
Периметр — первая линия защиты объекта. Задача системы: обнаружить человека или транспорт, пересекающий границу охраняемой территории, в любое время суток, при любой погоде, с задержкой до тревоги не более 3–5 секунд и минимумом ложных срабатываний.
Ложные тревоги от ветра, животных, теней и перепадов освещения — главная причина, по которой операторы отключают аналитику и переходят обратно к ручному просмотру. Проектируем систему так, чтобы этого не происходило.
Многоуровневая детекция вторжения
import cv2
import numpy as np
from ultralytics import YOLO
from typing import Optional
class PerimeterMonitor:
def __init__(self, model_path: str, perimeter_zones: dict):
self.detector = YOLO(model_path) # YOLOv8m или YOLOv9c
self.zones = perimeter_zones
# Background subtractor — первый уровень, быстрый
self.bg_sub = cv2.createBackgroundSubtractorMOG2(
history=300, varThreshold=25, detectShadows=True
)
self.target_classes = [0, 2, 3, 5, 7] # person, car, motorcycle, bus, truck
self.min_bbox_area = 1500 # фильтр мелких животных
def _motion_precheck(self, frame: np.ndarray) -> bool:
fg = self.bg_sub.apply(frame)
fg[fg == 127] = 0 # убираем тени
motion_ratio = np.count_nonzero(fg) / fg.size
return motion_ratio > 0.0015 # порог движения
def _in_perimeter(self, cx: int, cy: int, zone_name: str) -> bool:
poly = np.array(self.zones[zone_name]['polygon'], dtype=np.int32)
return cv2.pointPolygonTest(poly, (float(cx), float(cy)), False) >= 0
def analyze(self, frame: np.ndarray) -> list[dict]:
if not self._motion_precheck(frame):
return [] # нет движения — пропускаем дорогой инференс
results = self.detector.track(frame, persist=True,
classes=self.target_classes, conf=0.45)
events = []
for box in results[0].boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
area = (x2 - x1) * (y2 - y1)
if area < self.min_bbox_area:
continue # кот, птица, мусор
cx, cy = (x1 + x2) // 2, (y1 + y2) // 2
cls_name = self.detector.model.names[int(box.cls)]
for zone_name, zone_cfg in self.zones.items():
if self._in_perimeter(cx, cy, zone_name):
events.append({
'zone': zone_name,
'class': cls_name,
'confidence': float(box.conf),
'bbox': [x1, y1, x2, y2],
'track_id': int(box.id) if box.id else None,
'alert_level': zone_cfg.get('alert_level', 'warning')
})
return events
Ночная работа и сложные условия
Большинство реальных вторжений происходит ночью. Стандартные RGB-модели теряют точность при освещённости ниже 5 lux.
| Условие | Решение | Точность |
|---|---|---|
| Ночь с ИК-подсветкой | Дообучение на ИК-изображениях | 88–93% |
| Тепловая камера (FLIR) | Отдельная модель или fusion | 91–96% |
| Туман, дождь | Деградация изображения + preprocessing | 80–87% |
| Прямой свет фар | HDR-камера + выдержка | 85–92% |
| Снег, листопад | Kalman-фильтр трека + temporal | 88–93% |
Для критичных объектов рекомендуется мультиспектральный fusion: RGB + тепловая камера. YOLOv8 с двухканальным входом (RGB + thermal) на датасете FLIR ADAS даёт [email protected] = 0.87 против 0.71 для только RGB в ночных условиях.
Трекинг пересечения линии
class LineCrossingDetector:
def __init__(self, line: tuple, direction: str = 'both'):
# line = ((x1,y1), (x2,y2))
self.line = line
self.direction = direction # 'in', 'out', 'both'
self.track_history: dict[int, list] = {}
def check_crossing(self, track_id: int,
current_pos: tuple) -> Optional[str]:
if track_id not in self.track_history:
self.track_history[track_id] = []
history = self.track_history[track_id]
history.append(current_pos)
if len(history) < 2:
return None
prev, curr = history[-2], history[-1]
if self._segments_intersect(prev, curr, self.line[0], self.line[1]):
cross_dir = self._crossing_direction(prev, curr)
if self.direction == 'both' or cross_dir == self.direction:
return cross_dir
# Хранить только 30 точек истории
self.track_history[track_id] = history[-30:]
return None
Кейс: промышленный объект, 3 км периметра, 24 камеры
Объект — металлургический завод, периметр 3 км. Прежняя система: PIR-датчики + охранники. Ложные тревоги от диких животных (лисы, собаки) — 30–50 в ночь.
После развёртывания:
- YOLOv8m, дообученный на 800 дополнительных изображениях с животными (negative class)
- Фильтр по площади bbox: животные < 3500 px², люди/транспорт > 5000 px² при расстоянии 15–30м
- Тепловые камеры Axis Q1961-TE на 6 критичных участках
Результат за первый месяц: FAR снизился с 40+ до 2–3 событий за ночь, все — реальные попытки. Recall на контрольных тестах (30 инсценированных пересечений): 97%.
Инфраструктура: 3 сервера NVIDIA RTX 4090 (по 8 камер каждый), Apache Kafka для стриминга событий, интеграция с PSIM Genetec.
| Масштаб | Срок |
|---|---|
| До 8 камер, пилот | 3–5 недель |
| 8–30 камер, полноценный периметр | 7–12 недель |
| 30+ камер, enterprise-объект | 14–22 недели |







