Compare commits
No commits in common. "60002a90a7718581ec278aa0c55b93887b5003bd" and "7128413c7fd24a3773b43fbb980e1e0204f12d66" have entirely different histories.
60002a90a7
...
7128413c7f
5 changed files with 68 additions and 403 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,15 +0,0 @@
|
|||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
.lock
|
||||
|
||||
# Logs del runtime Hailo (se regeneran en cada ejecución)
|
||||
hailort.log
|
||||
hailort.1.log
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.13
|
||||
78
Readme.md
78
Readme.md
|
|
@ -1,23 +1,15 @@
|
|||
Pet Monitor con Raspberry Pi 5 & Hailo-8 (AI HAT+)
|
||||
Sistema de monitorización inteligente para mascotas utilizando aceleración por hardware NPU (chip Hailo-8) para visión por computador y detección de audio local en CPU para ladridos/sonidos anómalos.
|
||||
Pet Monitor con Raspberry Pi 5 & Hailo-8L (AI HAT)
|
||||
Sistema de monitorización inteligente para mascotas utilizando aceleración por hardware NPU (chip Hailo-8L) para visión por computador a 30 FPS y detección de audio local en CPU para ladridos/sonidos anómalos.
|
||||
---
|
||||
1. Hardware Utilizado
|
||||
Placa: Raspberry Pi 5.
|
||||
Coprocesador IA: Raspberry Pi AI HAT (Hailo-8, 26 TOPS).
|
||||
Cámara: cámara IP TP-Link Tapo accesible por RTSP en la red local.
|
||||
Coprocesador IA: Raspberry Pi AI HAT (Hailo-8L, 13 TOPS).
|
||||
Cámara: Módulo de cámara Raspberry Pi (CSI).
|
||||
Entrada de audio: Micrófono USB (pendiente de conectar).
|
||||
|
||||
> **Corrección respecto a la primera versión de este documento:** el módulo instalado identifica como Hailo-8, no Hailo-8L. Verificado con `hailortcli fw-control identify`:
|
||||
> ```
|
||||
> Board Name: Hailo-8
|
||||
> Device Architecture: HAILO8
|
||||
> Firmware Version: 4.23.0
|
||||
> ```
|
||||
> Por eso el modelo cargado es `yolov8s_h8.hef` (el de 8 carriles de cómputo de la NPU), no la variante `_h8l`.
|
||||
---
|
||||
2. Configuración del Sistema y Hardware
|
||||
Habilitar PCIe Gen 3
|
||||
Para maximizar el ancho de banda con el chip Hailo, se configuró el bus PCIe en `/boot/firmware/config.txt`:
|
||||
Para maximizar el ancho de banda con el chip Hailo-8L, se configuró el bus PCIe en `/boot/firmware/config.txt`:
|
||||
```ini
|
||||
dtparam=pciex1
|
||||
dtparam=pciex1_gen=3
|
||||
|
|
@ -30,30 +22,10 @@ sudo apt install -y hailo-all rpicam-apps libportaudio2 portaudio19-dev libasoun
|
|||
```
|
||||
Verificación de hardware
|
||||
NPU Hailo: `lspci` muestra el coprocesador Hailo Technologies y `hailortcli fw-control identify` responde con el firmware cargado.
|
||||
Cámara: `ffprobe -rtsp_transport tcp rtsp://usuario:clave@IP:554/stream2` debe describir un stream H.264.
|
||||
Cámara: `rpicam-hello --list-cameras` detecta correctamente el sensor CSI.
|
||||
---
|
||||
3. Cámara IP Tapo (RTSP)
|
||||
La cámara Tapo **no expone RTSP con la cuenta de TP-Link**: hay que crear una cuenta local de cámara en la app Tapo (Ajustes de la cámara → Configuración avanzada → Cuenta de cámara). El usuario y la contraseña de esa cuenta son los que van en el `.env`.
|
||||
|
||||
Streams disponibles (verificados con `ffprobe`):
|
||||
- `stream1`: 2304x1296 @ 15 fps (máxima calidad)
|
||||
- `stream2`: 1280x720 @ 15 fps (el que usa el monitor: menos CPU de decodificación)
|
||||
- `stream3`: no existe en este modelo (devuelve 404)
|
||||
|
||||
El vídeo va por H.264, así que la decodificación es por software (la Pi 5 solo acelera HEVC por hardware). A 720p el coste es asumible; con `stream1` sube bastante sin mejorar la detección, porque el modelo trabaja a 640x640 en cualquier caso.
|
||||
|
||||
Credenciales en `~/pet_monitor/.env` (no se versiona):
|
||||
```
|
||||
CAMERA_IP=192.168.178.234
|
||||
CAMERA_USER=camera_dog
|
||||
CAMERA_PW=...
|
||||
# Opcionales:
|
||||
# CAMERA_STREAM=stream2 # stream1 | stream2
|
||||
# CAMERA_RTSP_URL=rtsp://... # URL completa; tiene prioridad sobre lo anterior
|
||||
```
|
||||
---
|
||||
4. Entorno de Desarrollo Python (`uv`)
|
||||
Se configuró el entorno virtual utilizando `uv` con acceso a los paquetes del sistema (`--system-site-packages`) para reutilizar los bindings compilados de `picamera2`, `hailo_platform` y `OpenCV` instalados por Debian/Raspberry Pi OS:
|
||||
3. Entorno de Desarrollo Python (`uv`)
|
||||
Se configuró el entorno virtual utilizando `uv` con acceso a los paquetes del sistema (`--system-site-packages`) para reutilizar los bindings compilados de `picamera2` y `hailo_platform` instalados por Debian/Raspberry Pi OS:
|
||||
```bash
|
||||
cd ~/pet_monitor
|
||||
# Creación del entorno con acceso a librerías nativas
|
||||
|
|
@ -63,33 +35,19 @@ uv venv --system-site-packages
|
|||
uv pip install sounddevice numpy scipy requests onnxruntime
|
||||
```
|
||||
> **Nota técnica:** Se seleccionó `onnxruntime` en lugar de `tflite-runtime` para garantizar compatibilidad con versiones modernas de Python en ARM64 sin necesidad de compilar desde fuente.
|
||||
|
||||
> **Nota:** `uv` vive en `~/.local/bin/uv` y no está en el PATH de sesiones SSH no interactivas; usa `bash -lc 'uv run ...'` o `~/.local/bin/uv run ...`.
|
||||
---
|
||||
5. Estado Actual del Software
|
||||
`pet_speed.py`: Script de visión artificial optimizado con aceleración por hardware que:
|
||||
- Lee el vídeo de la cámara Tapo por **RTSP en un hilo aparte** (`RtspVideoStream`), que se queda siempre con el fotograma más reciente y descarta los antiguos. Así no se acumula latencia si el análisis va más lento que la cámara.
|
||||
- Se **reconecta automáticamente** con retroceso exponencial (2 s → 30 s) si la cámara se cae, se reinicia o cambia de IP.
|
||||
- Escala el fotograma con **letterbox** (mantiene la relación de aspecto, relleno gris) hasta los 640x640 que espera el modelo, y deshace esa transformación para dar las cajas y el centroide en píxeles del fotograma original.
|
||||
- Utiliza la API nativa de aceleración `picamera2.devices.hailo.Hailo` (parte del paquete del sistema, no requiere la cámara CSI) cargando el modelo optimizado (`/usr/share/hailo-models/yolov8s_h8.hef`).
|
||||
- Filtra detecciones del conjunto COCO para gato (id: 15) y perro (id: 16).
|
||||
- Calcula el centroide del animal y evalúa el desplazamiento entre fotogramas consecutivos para alertar si la velocidad supera el umbral de carrera (`SPEED_THRESHOLD = 0.30` por segundo).
|
||||
- Imprime estadísticas cada 10 s (fps de captura, fps de análisis, latencia de inferencia, reconexiones).
|
||||
- Libera el stream RTSP y el acelerador Hailo al recibir `SIGINT` (`Ctrl+C`) o `SIGTERM`, de cara a ejecutarlo como servicio systemd.
|
||||
|
||||
Rendimiento medido en la Pi 5 con `stream2`:
|
||||
```
|
||||
[STATS] captura 14.7 fps | análisis 12-15 fps | inferencia 11.8 ms | reconexiones 0
|
||||
```
|
||||
|
||||
Nota sobre el orden de color: los HEF de YOLOv8 esperan **RGB**, y así se alimenta al modelo. Comprobado con imágenes de prueba: con un perro la confianza pasa de 0.42 (BGR) a 0.85 (RGB), y con un gato de 0.79 a 0.87. Si algún día hiciera falta comparar, `PET_COLOR_ORDER=bgr uv run pet_speed.py`.
|
||||
|
||||
Ejecución:
|
||||
4. Estado Actual del Software
|
||||
`pet_speed.py`: Script de visión artificial que:
|
||||
Inicializa `Picamera2` a 1280x720 a 30 FPS.
|
||||
Carga el pipeline oficial de Hailo (`/usr/share/rpi-camera-assets/hailo_yolov8_inference.json`).
|
||||
Filtra detecciones de COCO para gato (id: 15) y perro (id: 16).
|
||||
Calcula el centroide del animal y evalúa el desplazamiento entre frames consecutivos para alertar si la velocidad supera el umbral de carrera (`SPEED_THRESHOLD = 0.30`).
|
||||
Ejecución actual:
|
||||
```bash
|
||||
uv run pet_speed.py
|
||||
uv run python pet_speed.py
|
||||
```
|
||||
---
|
||||
6. Próximos Pasos (Hoja de Ruta)
|
||||
5. Próximos Pasos (Hoja de Ruta)
|
||||
Fase A: Integración de Audio (Ladridos y Sonidos)
|
||||
Conectar el micrófono USB.
|
||||
Identificar el índice del dispositivo con:
|
||||
|
|
@ -101,7 +59,7 @@ Desarrollar el listener en un hilo secundario (`threading.Thread`) analizando fr
|
|||
Fase B: Sistema de Alertas y Notificaciones
|
||||
Crear un bot en Telegram con `@BotFather` y obtener el `TELEGRAM_BOT_TOKEN` y `CHAT_ID`.
|
||||
Programar un despachador asíncrono con control de enfriamiento (cooldown de 30-60 s) para evitar spam.
|
||||
Guardar una captura JPEG con la caja delimitadora en el momento de la carrera o ladrido y enviarla a Telegram (las cajas ya se calculan en píxeles del fotograma original, solo falta dibujarlas y guardarlas).
|
||||
Guardar una captura JPEG con la caja delimitadora en el momento de la carrera o ladrido y enviarla a Telegram.
|
||||
Fase C: Despliegue Autónomo (Systemd)
|
||||
Crear una unidad de servicio `/etc/systemd/system/pet_monitor.service`.
|
||||
Habilitar arranque automático al encender la Raspberry Pi:
|
||||
|
|
|
|||
0
hailort.log
Normal file
0
hailort.log
Normal file
371
pet_speed.py
371
pet_speed.py
|
|
@ -1,362 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pet Monitor - Visión por computador con cámara IP Tapo (RTSP) + NPU Hailo-8.
|
||||
|
||||
Sustituye la cámara CSI interna de la Raspberry Pi por la cámara Tapo de red:
|
||||
|
||||
- Ingesta RTSP en un hilo aparte, que descarta los frames viejos y expone
|
||||
siempre el más reciente (evita la latencia acumulada típica de RTSP).
|
||||
- Reconexión automática con backoff si la cámara se cae o cambia de IP.
|
||||
- Inferencia YOLOv8s sobre la NPU Hailo (HEF del sistema).
|
||||
- Detección de gato/perro (clases COCO 15 y 16) y alerta de carrera según
|
||||
el desplazamiento del centroide entre frames consecutivos.
|
||||
|
||||
Variables en .env (junto a este script):
|
||||
CAMERA_IP, CAMERA_USER, CAMERA_PW (obligatorias: cuenta de cámara Tapo)
|
||||
CAMERA_STREAM (opcional: stream2 = 720p, stream1 = 2K)
|
||||
CAMERA_RTSP_URL (opcional: URL RTSP completa, tiene prioridad)
|
||||
|
||||
Ejecución: uv run pet_speed.py
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import math
|
||||
from picamera2 import Picamera2
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from picamera2.devices.hailo import Hailo, hailo_architecture
|
||||
|
||||
# --- Configuración -------------------------------------------------------
|
||||
# Clases COCO: 15 = Cat (Gato), 16 = Dog (Perro)
|
||||
TARGET_CLASSES = {15: "Gato", 16: "Perro"}
|
||||
CONF_THRESHOLD = 0.45 # Confianza mínima para considerar una detección
|
||||
SPEED_THRESHOLD = 0.30 # Velocidad normalizada por segundo (fracción del frame)
|
||||
DEFAULT_STREAM = "stream2" # stream1 = 2304x1296@15, stream2 = 1280x720@15
|
||||
STATS_INTERVAL = 10.0 # Segundos entre líneas de estadísticas
|
||||
SPEED_THRESHOLD = 0.30
|
||||
|
||||
# Orden de color que espera el HEF. Los modelos de Hailo Model Zoo se compilan
|
||||
# para RGB; con PET_COLOR_ORDER=bgr se invierte (útil para comparar precisión).
|
||||
COLOR_ORDER = os.environ.get("PET_COLOR_ORDER", "rgb").lower()
|
||||
|
||||
ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
||||
LETTERBOX_FILL = (114, 114, 114)
|
||||
|
||||
# Latencia baja en RTSP: forzar TCP evita el jitter y las pérdidas de UDP.
|
||||
# (No se usa 'stimeout': fue eliminado en FFmpeg 6, que es el que trae OpenCV 4.10)
|
||||
os.environ.setdefault("OPENCV_FFMPEG_CAPTURE_OPTIONS", "rtsp_transport;tcp")
|
||||
|
||||
|
||||
# --- Utilidades ----------------------------------------------------------
|
||||
def load_env(path=ENV_PATH):
|
||||
"""Lee un .env sencillo (CLAVE=valor) sin dependencias externas."""
|
||||
env = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
env[key.strip()] = value.strip().strip('"').strip("'")
|
||||
# Las variables de entorno reales tienen prioridad sobre el fichero, para
|
||||
# poder sobreescribirlas desde systemd o desde una prueba puntual.
|
||||
for key in ("CAMERA_IP", "CAMERA_USER", "CAMERA_PW", "CAMERA_STREAM", "CAMERA_RTSP_URL"):
|
||||
if os.environ.get(key):
|
||||
env[key] = os.environ[key]
|
||||
return env
|
||||
|
||||
|
||||
def mask_url(url):
|
||||
"""Oculta las credenciales de la URL para poder mostrarla por consola."""
|
||||
parts = urllib.parse.urlsplit(url)
|
||||
if not parts.hostname:
|
||||
return "***"
|
||||
port = f":{parts.port}" if parts.port else ""
|
||||
return f"{parts.scheme}://***@{parts.hostname}{port}{parts.path}"
|
||||
|
||||
|
||||
def build_rtsp_url(env, stream):
|
||||
"""Construye la URL RTSP de la Tapo, escapando usuario y contraseña."""
|
||||
if env.get("CAMERA_RTSP_URL"):
|
||||
return env["CAMERA_RTSP_URL"]
|
||||
ip = env.get("CAMERA_IP")
|
||||
user = env.get("CAMERA_USER")
|
||||
password = env.get("CAMERA_PW")
|
||||
missing = [k for k, v in (("CAMERA_IP", ip), ("CAMERA_USER", user), ("CAMERA_PW", password)) if not v]
|
||||
if missing:
|
||||
raise SystemExit(f"[ERROR] Faltan variables en {ENV_PATH}: {', '.join(missing)}")
|
||||
return "rtsp://%s:%s@%s:554/%s" % (
|
||||
urllib.parse.quote(user, safe=""),
|
||||
urllib.parse.quote(password, safe=""),
|
||||
ip,
|
||||
stream,
|
||||
)
|
||||
|
||||
|
||||
def get_hailo_model_path():
|
||||
"""Elige el HEF de YOLOv8s que corresponde a la arquitectura detectada."""
|
||||
arch = hailo_architecture()
|
||||
if arch == "HAILO8":
|
||||
candidates = ["/usr/share/hailo-models/yolov8s_h8.hef", "/usr/share/hailo-models/yolov8s_h8l.hef"]
|
||||
def calculate_centroid(box):
|
||||
if isinstance(box, dict):
|
||||
x = box.get("x", 0.0) + (box.get("width", 0.0) / 2.0)
|
||||
y = box.get("y", 0.0) + (box.get("height", 0.0) / 2.0)
|
||||
elif isinstance(box, (list, tuple)) and len(box) >= 4:
|
||||
x = (box[0] + box[2]) / 2.0
|
||||
y = (box[1] + box[3]) / 2.0
|
||||
else:
|
||||
candidates = ["/usr/share/hailo-models/yolov8s_h8l.hef", "/usr/share/hailo-models/yolov8s_h8.hef"]
|
||||
for path in candidates:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
raise FileNotFoundError("No se encontró ningún modelo YOLOv8 HEF en /usr/share/hailo-models/")
|
||||
x, y = 0.5, 0.5
|
||||
return x, y
|
||||
|
||||
|
||||
def letterbox(frame, width, height):
|
||||
"""
|
||||
Reescala manteniendo la relación de aspecto y rellena hasta width x height.
|
||||
Devuelve (imagen, escala, pad_x, pad_y) para poder deshacer la transformación.
|
||||
"""
|
||||
src_h, src_w = frame.shape[:2]
|
||||
scale = min(width / src_w, height / src_h)
|
||||
new_w, new_h = int(round(src_w * scale)), int(round(src_h * scale))
|
||||
resized = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
|
||||
pad_x, pad_y = (width - new_w) // 2, (height - new_h) // 2
|
||||
padded = cv2.copyMakeBorder(
|
||||
resized, pad_y, height - new_h - pad_y, pad_x, width - new_w - pad_x,
|
||||
cv2.BORDER_CONSTANT, value=LETTERBOX_FILL,
|
||||
)
|
||||
return padded, scale, pad_x, pad_y
|
||||
|
||||
|
||||
def box_to_source(box, scale, pad_x, pad_y, src_w, src_h):
|
||||
"""
|
||||
Convierte una caja normalizada del modelo (letterbox) a píxeles del frame
|
||||
original. Hailo NMS devuelve [ymin, xmin, ymax, xmax, confidence].
|
||||
"""
|
||||
ymin, xmin, ymax, xmax = box[:4]
|
||||
model_w = src_w * scale + 2 * pad_x
|
||||
model_h = src_h * scale + 2 * pad_y
|
||||
x1 = (xmin * model_w - pad_x) / scale
|
||||
x2 = (xmax * model_w - pad_x) / scale
|
||||
y1 = (ymin * model_h - pad_y) / scale
|
||||
y2 = (ymax * model_h - pad_y) / scale
|
||||
clip_x = lambda v: float(min(max(v, 0.0), src_w)) # noqa: E731
|
||||
clip_y = lambda v: float(min(max(v, 0.0), src_h)) # noqa: E731
|
||||
return clip_x(x1), clip_y(y1), clip_x(x2), clip_y(y2)
|
||||
|
||||
|
||||
# --- Ingesta de vídeo ----------------------------------------------------
|
||||
class RtspVideoStream:
|
||||
"""
|
||||
Hilo lector de RTSP: mantiene siempre el último frame disponible.
|
||||
|
||||
Si el bucle principal va más lento que la cámara, los frames intermedios se
|
||||
descartan solos (no se encolan), así la detección trabaja sobre imagen en
|
||||
vivo y no sobre un buffer con retardo creciente.
|
||||
"""
|
||||
|
||||
def __init__(self, url, reconnect_delay=2.0, max_reconnect_delay=30.0):
|
||||
self._url = url
|
||||
self._reconnect_delay = reconnect_delay
|
||||
self._max_reconnect_delay = max_reconnect_delay
|
||||
self._lock = threading.Lock()
|
||||
self._new_frame = threading.Event()
|
||||
self._stop = threading.Event()
|
||||
self._thread = None
|
||||
self._seq = 0
|
||||
self._frame = None
|
||||
self._timestamp = 0.0
|
||||
self.frames_read = 0
|
||||
self.reconnects = 0
|
||||
self.connected = False
|
||||
self.capture_fps = 0.0
|
||||
|
||||
def start(self):
|
||||
self._thread = threading.Thread(target=self._loop, name="rtsp-reader", daemon=True)
|
||||
self._thread.start()
|
||||
return self
|
||||
|
||||
def _open_capture(self):
|
||||
cap = cv2.VideoCapture(self._url, cv2.CAP_FFMPEG)
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
if not cap.isOpened():
|
||||
cap.release()
|
||||
return None
|
||||
return cap
|
||||
|
||||
def _loop(self):
|
||||
cap = None
|
||||
delay = self._reconnect_delay
|
||||
window_start = time.time()
|
||||
window_frames = 0
|
||||
|
||||
while not self._stop.is_set():
|
||||
if cap is None:
|
||||
cap = self._open_capture()
|
||||
if cap is None:
|
||||
self.connected = False
|
||||
print(f"[WARN] No hay stream RTSP; reintentando en {delay:.0f}s", flush=True)
|
||||
self._stop.wait(delay)
|
||||
delay = min(delay * 2, self._max_reconnect_delay)
|
||||
self.reconnects += 1
|
||||
continue
|
||||
self.connected = True
|
||||
delay = self._reconnect_delay
|
||||
window_start, window_frames = time.time(), 0
|
||||
print("[INFO] Stream RTSP conectado.", flush=True)
|
||||
|
||||
ok, frame = cap.read()
|
||||
if not ok or frame is None:
|
||||
print("[WARN] Lectura RTSP interrumpida; reconectando...", flush=True)
|
||||
cap.release()
|
||||
cap = None
|
||||
self.connected = False
|
||||
self.reconnects += 1
|
||||
self._stop.wait(delay)
|
||||
continue
|
||||
|
||||
now = time.time()
|
||||
window_frames += 1
|
||||
elapsed = now - window_start
|
||||
if elapsed >= 2.0:
|
||||
self.capture_fps = window_frames / elapsed
|
||||
window_start, window_frames = now, 0
|
||||
|
||||
with self._lock:
|
||||
self._seq += 1
|
||||
self._frame = frame
|
||||
self._timestamp = now
|
||||
self.frames_read += 1
|
||||
self._new_frame.set()
|
||||
|
||||
if cap is not None:
|
||||
cap.release()
|
||||
|
||||
def wait_for_frame(self, last_seq, timeout=1.0):
|
||||
"""
|
||||
Bloquea hasta que haya un frame más nuevo que last_seq.
|
||||
Devuelve (seq, frame, timestamp) o (last_seq, None, None) si expira.
|
||||
"""
|
||||
deadline = time.time() + timeout
|
||||
while not self._stop.is_set() and time.time() < deadline:
|
||||
with self._lock:
|
||||
seq, frame, ts = self._seq, self._frame, self._timestamp
|
||||
if seq > last_seq and frame is not None:
|
||||
return seq, frame, ts
|
||||
self._new_frame.wait(0.05)
|
||||
self._new_frame.clear()
|
||||
return last_seq, None, None
|
||||
|
||||
def stop(self):
|
||||
self._stop.set()
|
||||
self._new_frame.set()
|
||||
if self._thread is not None:
|
||||
self._thread.join(timeout=5.0)
|
||||
|
||||
|
||||
# --- Bucle principal -----------------------------------------------------
|
||||
def main():
|
||||
env = load_env()
|
||||
stream_name = env.get("CAMERA_STREAM", DEFAULT_STREAM)
|
||||
url = build_rtsp_url(env, stream_name)
|
||||
picam2 = Picamera2()
|
||||
|
||||
model_path = get_hailo_model_path()
|
||||
print(f"[INFO] Cargando modelo Hailo: {model_path} ...")
|
||||
hailo = Hailo(model_path)
|
||||
model_h, model_w, _ = hailo.get_input_shape()
|
||||
print(f"[INFO] Modelo cargado. Resolución de entrada: {model_w}x{model_h}")
|
||||
post_process_file = "/usr/share/rpi-camera-assets/hailo_yolov8_inference.json"
|
||||
|
||||
# Ocultamos la contraseña al mostrarla por consola
|
||||
print(f"[INFO] Cámara Tapo: {mask_url(url)}")
|
||||
# Se inyecta la etapa Hailo directamente en la configuración de vídeo
|
||||
config = picam2.create_video_configuration(
|
||||
main={"size": (1280, 720), "format": "RGB888"},
|
||||
controls={"FrameRate": 30},
|
||||
post_process_file=post_process_file
|
||||
)
|
||||
|
||||
stream = RtspVideoStream(url).start()
|
||||
print("[INFO] Monitorizando mascotas (Gatos y Perros)... Presiona Ctrl+C para salir.\n")
|
||||
picam2.configure(config)
|
||||
picam2.start()
|
||||
|
||||
running = threading.Event()
|
||||
running.set()
|
||||
print("[INFO] Pipeline Picamera2 + Hailo-8L activo.")
|
||||
|
||||
def handle_signal(signum, _frame):
|
||||
print(f"\n[INFO] Señal {signum} recibida; deteniendo...", flush=True)
|
||||
running.clear()
|
||||
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
|
||||
last_seq = 0
|
||||
last_centroid = None
|
||||
last_frame_time = 0.0
|
||||
last_stats = time.time()
|
||||
frames_processed = 0
|
||||
inference_time = 0.0
|
||||
last_time = time.time()
|
||||
|
||||
try:
|
||||
while running.is_set():
|
||||
seq, frame, frame_time = stream.wait_for_frame(last_seq)
|
||||
if frame is None:
|
||||
continue
|
||||
last_seq = seq
|
||||
while True:
|
||||
metadata = picam2.capture_metadata()
|
||||
current_time = time.time()
|
||||
dt = current_time - last_time
|
||||
|
||||
letterboxed, scale, pad_x, pad_y = letterbox(frame, model_w, model_h)
|
||||
if COLOR_ORDER == "bgr":
|
||||
model_input = np.ascontiguousarray(letterboxed)
|
||||
else:
|
||||
model_input = np.ascontiguousarray(cv2.cvtColor(letterboxed, cv2.COLOR_BGR2RGB))
|
||||
|
||||
started = time.time()
|
||||
detections_by_class = hailo.run(model_input)
|
||||
inference_time += time.time() - started
|
||||
frames_processed += 1
|
||||
|
||||
src_h, src_w = frame.shape[:2]
|
||||
dt = frame_time - last_frame_time if last_frame_time else 0.0
|
||||
detections = metadata.get("hailo_detections", [])
|
||||
|
||||
pet_found = False
|
||||
for class_id, label in TARGET_CLASSES.items():
|
||||
if class_id >= len(detections_by_class):
|
||||
continue
|
||||
for det in detections_by_class[class_id]:
|
||||
confidence = det[4]
|
||||
if confidence <= CONF_THRESHOLD:
|
||||
continue
|
||||
for det in detections:
|
||||
class_id = det.get("class_id")
|
||||
confidence = det.get("confidence", 0.0)
|
||||
|
||||
if class_id in TARGET_CLASSES and confidence > 0.45:
|
||||
pet_found = True
|
||||
x1, y1, x2, y2 = box_to_source(det, scale, pad_x, pad_y, src_w, src_h)
|
||||
centroid = ((x1 + x2) / 2.0 / src_w, (y1 + y2) / 2.0 / src_h)
|
||||
label = TARGET_CLASSES[class_id]
|
||||
box = det.get("box", [0, 0, 0, 0])
|
||||
|
||||
centroid = calculate_centroid(box)
|
||||
|
||||
if last_centroid and dt > 0:
|
||||
dist = math.hypot(centroid[0] - last_centroid[0], centroid[1] - last_centroid[1])
|
||||
dx = centroid[0] - last_centroid[0]
|
||||
dy = centroid[1] - last_centroid[1]
|
||||
dist = math.hypot(dx, dy)
|
||||
speed = dist / dt
|
||||
|
||||
if speed > SPEED_THRESHOLD:
|
||||
print(f"⚠️ [ALERTA] ¡{label} corriendo! Vel: {speed:.2f} norm/s (Conf: {confidence:.2f})")
|
||||
else:
|
||||
print(f"🐾 [OK] {label} detectado (calmado). Vel: {speed:.2f} norm/s (Conf: {confidence:.2f})")
|
||||
print(f"[OK] {label} detectado (calmado). Vel: {speed:.2f} norm/s")
|
||||
|
||||
last_centroid = centroid
|
||||
break # Evaluamos la mascota principal detectada
|
||||
if pet_found:
|
||||
break
|
||||
|
||||
if not pet_found:
|
||||
last_centroid = None
|
||||
|
||||
last_frame_time = frame_time
|
||||
|
||||
now = time.time()
|
||||
if now - last_stats >= STATS_INTERVAL:
|
||||
avg_inf = inference_time / frames_processed * 1000 if frames_processed else 0.0
|
||||
print(
|
||||
f"[STATS] captura {stream.capture_fps:.1f} fps | análisis "
|
||||
f"{frames_processed / (now - last_stats):.1f} fps | inferencia {avg_inf:.1f} ms "
|
||||
f"| reconexiones {stream.reconnects}"
|
||||
)
|
||||
last_stats = now
|
||||
frames_processed = 0
|
||||
inference_time = 0.0
|
||||
last_time = current_time
|
||||
time.sleep(0.03)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n[INFO] Deteniendo monitor...")
|
||||
finally:
|
||||
stream.stop()
|
||||
hailo.close()
|
||||
print("[INFO] Stream RTSP cerrado y acelerador Hailo liberado correctamente.")
|
||||
|
||||
picam2.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue