Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Скопируйте в .env и заполните. Файл .env не попадает в git.

# Telegram: токен от @BotFather и ID чата (узнать можно у @userinfobot)
ECOWATCH_TG_TOKEN=
ECOWATCH_TG_CHAT_ID=

# Координаты камеры — для карты инцидентов (необязательно)
ECOWATCH_CAMERA_LAT=
ECOWATCH_CAMERA_LON=

# Необязательно: путь к модели, папка данных, устройство (cpu / 0 для GPU)
# ECOWATCH_MODEL=models/best.pt
# ECOWATCH_DATA_DIR=data
# ECOWATCH_DEVICE=0
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: tests

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
# Тесты не требуют ultralytics/torch — ставим только лёгкие зависимости.
- run: pip install numpy opencv-python-headless Pillow requests -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: pytest -q
25 changes: 13 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
.venv/
venv/
.idea/
.vscode/
__pycache__/
*.pyc
.pytest_cache/
.ruff_cache/

# секреты
.env

# данные приложения: журнал, снимки, клипы, загрузки
data/
recordings/
detection_history.json

# датасеты и результаты обучения
dataset_detect/
dataset_retrain/
fire_smoke_images/
runs/
runs_retrain/

shop project/

*.pt
!models/best.pt

recordings/
detection_history.json

1111.py
1111_backup*.py
check_classes.py
check_imports.py
download_extra_data.py
retrain_model.py
train_detect.py
15 changes: 15 additions & 0 deletions .streamlit/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[theme]
base = "dark"
primaryColor = "#ef4444"
backgroundColor = "#0f1117"
secondaryBackgroundColor = "#1a1d26"
textColor = "#e2e4eb"

[client]
toolbarMode = "minimal"

[browser]
gatherUsageStats = false

[server]
maxUploadSize = 1024
130 changes: 99 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,132 @@
# EcoWatch AI
# 🛡️ EcoWatch AI

Система обнаружения экологических угроз на основе YOLOv8.
Система обнаружения пожаров и дыма на видео в реальном времени на базе YOLOv8.

Анализирует видеопоток в реальном времени и определяет:
- 🔥 Пожар
- 💨 Дым
- 🗑️ Сжигание мусора
Работает с веб-камерой, IP-камерой (RTSP/HTTP), видеофайлом или захватом экрана.
Подтверждённые угрозы записываются в журнал со снимком и видеоклипом, а уведомления уходят в Telegram.

## Возможности

- Мониторинг с веб-камеры, IP-камеры, экрана или загруженного файла
- Уведомления в Telegram с фото
- Аналитика и графики
- Журнал обнаружений с экспортом в CSV
- Запись видео при обнаружении угрозы
- **Без ложных «миганий».** Вероятности сглаживаются по времени, а инцидент
начинается только после нескольких подряд опасных кадров и заканчивается после серии чистых.
Если дым перерастает в пожар, это фиксируется в том же инциденте.
- **Инциденты, а не спам.** Одна угроза — одна запись в журнале (начало, конец, пиковая
вероятность, снимок, клип) и одно уведомление в Telegram с настраиваемой паузой.
- **Фоновый мониторинг.** Видео обрабатывается в отдельном потоке: интерфейс не подвисает,
а мониторинг продолжается при переключении вкладок и перезагрузке страницы.
- **Клипы с предзаписью.** В клип попадают несколько секунд *до* тревоги.
- **Переподключение к камере** при обрыве связи.
- **Консольный режим** для круглосуточной работы на сервере без браузера.
- Аналитика по периодам, тепловая карта активности, карта камер, экспорт журнала в CSV.
- Поддерживаются модели classify (как текущая `models/best.pt`) и detect (с рамками).

## Установка

Нужен Python 3.10 или новее.

```bash
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux/macOS
pip install -r requirements.txt
```

### Настройка Telegram

Скопируйте `.env.example` в `.env` и заполните:

```ini
ECOWATCH_TG_TOKEN=123456:ABC... # токен от @BotFather
ECOWATCH_TG_CHAT_ID=123456789 # ID чата, можно узнать у @userinfobot
```

Токен никогда не хранится в коде: файл `.env` исключён из git.

## Запуск

### Веб-интерфейс

```bash
streamlit run main.py
```

Или через батник:
Или двойной клик по `run_app.bat` в Windows. Откроется http://localhost:8501.

### Консольный режим

```bash
python -m ecowatch 0 # веб-камера №0
python -m ecowatch rtsp://user:pass@192.168.1.10/live --record
python -m ecowatch video.mp4 --show # с окном предпросмотра
python -m ecowatch screen # захват экрана
python -m ecowatch report # отчёт за сегодня в Telegram
python -m ecowatch --help # все параметры
```
run_app.bat
```

Ежедневный отчёт удобно отправлять по расписанию: через планировщик заданий Windows или cron.

## Настройка чувствительности

| Параметр | По умолчанию | Смысл |
|---|---|---|
| Порог вероятности | 0.65 | минимальная сглаженная вероятность опасного класса |
| Отрыв от 2-го класса | 0.15 | защита от неоднозначных кадров |
| Сглаживание | 0.35 | вес нового кадра: меньше — стабильнее, но медленнее реакция |
| Кадров для подтверждения | 4 | сколько опасных кадров подряд нужно для тревоги |
| Кадров для отбоя | 15 | сколько чистых кадров подряд завершают инцидент |

В веб-интерфейсе пороги меняются на лету, без перезапуска мониторинга.

## Обучение модели

Подготовьте датасет в папке `dataset_detect/` со структурой:
**Классификация** (как у текущей модели): датасет из папок по классам.

```
dataset_detect/
├── train/
│ ├── fire/
│ ├── normal/
│ └── smoke/
└── valid/
├── fire/
├── normal/
└── smoke/
├── train/ {fire, normal, smoke}/
└── val/ {fire, normal, smoke}/
```

```bash
python train.py --data dataset_detect
```

Запустите:
**Детекция с рамками** (рекомендуется: точнее и показывает, *где* огонь): датасет в формате YOLO.

```bash
python train.py
python train.py --data fire_smoke/data.yaml --model yolov8s.pt --imgsz 640
```

Лучшие веса автоматически копируются в `models/best.pt`, а прежняя модель сохраняется
рядом как `best_<дата>.pt`.

## Структура проекта

```
main.py точка входа Streamlit
train.py обучение модели
ecowatch/
├── config.py настройки и пороги (.env)
├── detector.py обёртка YOLO: classify и detect
├── tracker.py сглаживание и гистерезис → инциденты
├── monitor.py фоновый цикл мониторинга
├── sources.py камеры, потоки, файлы, экран
├── storage.py журнал инцидентов (SQLite)
├── notifier.py Telegram (в фоне)
├── recorder.py клипы с предзаписью
├── overlay.py отрисовка поверх видео
├── ui.py веб-интерфейс
└── cli.py консольный режим
tests/ тесты (pytest)
```

## Стек
Данные хранятся в `data/`: база `ecowatch.db`, а также папки `snapshots/` и `clips/`.
Журнал старой версии (`detection_history.json`) импортируется автоматически при первом запуске.

- Python 3.11
- Streamlit
- YOLOv8 (ultralytics)
- OpenCV
- Plotly
## Разработка

```bash
pip install -r requirements-dev.txt
pytest
ruff check . && ruff format --check .
```
3 changes: 3 additions & 0 deletions ecowatch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""EcoWatch AI — обнаружение пожаров и дыма на видео с помощью YOLO."""

__version__ = "4.0.0"
5 changes: 5 additions & 0 deletions ecowatch/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys

from .cli import main

sys.exit(main())
114 changes: 114 additions & 0 deletions ecowatch/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""Консольный режим без браузера — для круглосуточной работы на сервере.

Примеры:
python -m ecowatch 0 # веб-камера 0
python -m ecowatch rtsp://user:pass@ip/stream --record
python -m ecowatch video.mp4 --show # с окном предпросмотра
python -m ecowatch report # отправить отчёт за сегодня в Telegram
"""

from __future__ import annotations

import argparse
import logging
import signal
import sys
import time

from .config import DetectionParams, Settings
from .notifier import AsyncSender, TelegramNotifier, format_daily_report
from .sources import parse_source
from .storage import EventStore


def build_parser() -> argparse.ArgumentParser:
d = DetectionParams()
p = argparse.ArgumentParser(prog="ecowatch", description="EcoWatch AI — мониторинг пожаров и дыма")
p.add_argument("source", help="0 (камера), rtsp://…, путь к видео, screen[:N] или 'report'")
p.add_argument("--model", help="путь к весам YOLO (по умолчанию models/best.pt)")
p.add_argument("--conf", type=float, default=d.conf_threshold, help="порог вероятности")
p.add_argument("--margin", type=float, default=d.margin, help="отрыв от второго класса")
p.add_argument("--confirm", type=int, default=d.confirm_frames, help="кадров для подтверждения")
p.add_argument("--clear", type=int, default=d.clear_frames, help="кадров для отбоя")
p.add_argument("--record", action="store_true", help="записывать клипы инцидентов")
p.add_argument("--no-telegram", action="store_true", help="не отправлять уведомления")
p.add_argument("--cooldown", type=float, default=60, help="пауза между тревогами, с")
p.add_argument("--show", action="store_true", help="показывать окно с видео")
p.add_argument("-v", "--verbose", action="store_true")
return p


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)-7s %(message)s",
datefmt="%H:%M:%S",
)
settings = Settings.from_env()
if args.model:
from pathlib import Path

settings.model_path = Path(args.model)
settings.ensure_dirs()
store = EventStore(settings.db_path)
notifier = TelegramNotifier(settings.telegram_token, settings.telegram_chat_id)

if args.source == "report":
if not notifier.enabled:
logging.error("Telegram не настроен (ECOWATCH_TG_TOKEN / ECOWATCH_TG_CHAT_ID)")
return 1
return 0 if notifier.send_message(format_daily_report(store.day_summary())) else 1

from .detector import Detector
from .monitor import Monitor, MonitorOptions

detector = Detector(settings.model_path, settings.device)
logging.info("Модель %s (%s): %s", settings.model_path.name, detector.task, ", ".join(detector.names))
options = MonitorOptions(
source=parse_source(args.source),
params=DetectionParams(
conf_threshold=args.conf, margin=args.margin, confirm_frames=args.confirm, clear_frames=args.clear
),
notify=not args.no_telegram and notifier.enabled,
notify_cooldown=args.cooldown,
record=args.record,
lat=settings.camera_lat,
lon=settings.camera_lon,
)
if not args.no_telegram and not notifier.enabled:
logging.warning("Telegram не настроен — уведомления отключены")
sender = AsyncSender()
monitor = Monitor(detector, store, settings, options, notifier, sender)

signal.signal(signal.SIGINT, lambda *_: monitor.stop(wait=False))
if hasattr(signal, "SIGTERM"):
signal.signal(signal.SIGTERM, lambda *_: monitor.stop(wait=False))

monitor.start()
logging.info("Мониторинг запущен: %s (Ctrl+C — остановить)", options.source.label)
last_log = 0.0
while monitor.alive:
s = monitor.state()
if args.show and s.frame is not None:
import cv2

cv2.imshow("EcoWatch AI", s.frame)
if cv2.waitKey(1) & 0xFF in (ord("q"), 27):
monitor.stop(wait=False)
else:
time.sleep(0.2)
if time.monotonic() - last_log > 30:
last_log = time.monotonic()
logging.info("кадров: %d · %.1f FPS · инцидентов: %d", s.frames, s.fps, s.incidents)
monitor.stop()
sender.join()
s = monitor.state()
if s.error:
logging.error("Ошибка: %s", s.error)
logging.info("Готово. Кадров: %d, инцидентов: %d", s.frames, s.incidents)
return 1 if s.error else 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading