La velocidad se medía con el salto del centroide entre dos fotogramas consecutivos. Con la Tapo a 15 fps eso daba falsos positivos por tres vías, todas comprobadas: mover la cámara (el aviso falso del 2026-10-01), el parpadeo de la caja del detector (31 alertas con la escena quieta) y el cambio de sujeto cuando hay dos animales (39 alertas con ambos inmóviles). - pet_motion.py: seguimiento temporal. Velocidad medida sobre una ventana de 0.2 s (el temblor se cancela, el movimiento real no), asociación con la detección más cercana a la anterior en vez de con la primera de la lista, descarte de saltos imposibles para un animal y exigencia de 3 fotogramas seguidos por encima del umbral. - pet_speed.py: detecta el cambio de escena comparando una miniatura del fotograma (calibrado con fotogramas reales: escena quieta 0.0001, mascota moviéndose hasta 0.062, encuadre desplazado un 5% ya 0.119) y reinicia el seguimiento en vez de avisar. - Readme.md: sección de detección de carreras, con las tres causas de falso positivo y el límite conocido (paneo rápido y sostenido).
193 lines
14 KiB
Markdown
193 lines
14 KiB
Markdown
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.
|
|
---
|
|
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.
|
|
Entrada de audio: micrófono integrado de la propia cámara Tapo (viaja en el stream RTSP). No hace falta micrófono USB.
|
|
|
|
> **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`:
|
|
```ini
|
|
dtparam=pciex1
|
|
dtparam=pciex1_gen=3
|
|
```
|
|
Paquetes del Sistema Instalados
|
|
Se instalaron las utilidades de cámara, controladores del kernel y librerías dinámicas de audio requeridas por el sistema operativo:
|
|
```bash
|
|
sudo apt update && sudo apt full-upgrade -y
|
|
sudo apt install -y hailo-all rpicam-apps libportaudio2 portaudio19-dev libasound2-dev python3-picamera2 python3-av
|
|
```
|
|
|
|
> `python3-av` (PyAV) es imprescindible para el audio: OpenCV no sabe decodificar la pista de sonido del RTSP. Se instala a nivel de sistema y el venv lo ve gracias a `--system-site-packages`.
|
|
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.
|
|
---
|
|
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
|
|
# PET_AUDIO=0 # desactiva la escucha de ladridos
|
|
# BARK_THRESHOLD=0.15 # umbral de la puntuación de Bark
|
|
# BARK_COOLDOWN=15 # segundos mínimos entre dos avisos de ladrido
|
|
```
|
|
---
|
|
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:
|
|
```bash
|
|
cd ~/pet_monitor
|
|
# Creación del entorno con acceso a librerías nativas
|
|
uv venv --system-site-packages
|
|
|
|
# Instalación de librerías en el entorno virtual
|
|
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
|
|
El proyecto se reparte en módulos, más los scripts de apoyo:
|
|
- `pet_common.py`: carga del `.env`, construcción de la URL RTSP y enmascarado de credenciales.
|
|
- `pet_speed.py`: visión por computador (vídeo + NPU Hailo). Es el punto de entrada.
|
|
- `pet_motion.py`: seguimiento del movimiento y decisión de "está corriendo".
|
|
- `pet_audio.py`: detección de ladridos sobre el audio de la cámara.
|
|
- `pet_alerts.py`: decide qué sale por consola, qué se guarda como captura y qué va a Telegram.
|
|
- `pet_telegram.py`: cliente de Telegram (cola + hilo de envío, reintentos).
|
|
- `scripts/fetch_models.sh`: descarga y verifica (SHA-256) el modelo YAMNet.
|
|
- `scripts/telegram_check.py`: puesta en marcha de Telegram (valida el token y deduce el chat_id).
|
|
|
|
`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 la velocidad de la mascota con un **seguimiento temporal** (`pet_motion.py`) y avisa si 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:
|
|
```bash
|
|
uv run pet_speed.py
|
|
```
|
|
---
|
|
6. Detección de Carreras (seguimiento temporal)
|
|
La primera versión medía el salto del centroide **entre dos fotogramas consecutivos**, sin más. Eso genera falsos positivos por tres vías, y las tres se comprobaron:
|
|
|
|
1. **Mover la cámara.** El detector encuentra "algo" antes y después de recolocar la cámara, y el salto entre ambas posiciones parece una carrera. Es el aviso falso que saltó el 2026-10-01 al mover la cámara de sitio.
|
|
2. **Parpadeo del detector.** Con la caja temblando ±2% del encuadre, la velocidad instantánea llega a 0.8 norm/s sin que nadie se mueva (medido: la lógica antigua daba 31 alertas en una escena completamente quieta).
|
|
3. **Cambio de sujeto.** Si hay dos animales y la lista de detecciones cambia de orden, el centroide salta de uno a otro (la lógica antigua daba 39 alertas con dos animales inmóviles).
|
|
|
|
`pet_motion.py` lo resuelve con cuatro medidas:
|
|
- **Velocidad sobre una ventana de 0.2 s**, no entre dos fotogramas: el temblor se cancela, el movimiento real no.
|
|
- **Asociación por cercanía**: cada fotograma se empareja con la detección más próxima a la anterior, en vez de con la primera de la lista.
|
|
- **Descarte de saltos imposibles** (`MAX_PLAUSIBLE_SPEED = 3.0` norm/s): cámara movida o cambio de sujeto.
|
|
- **Reinicio por cambio de escena**: el bucle compara una miniatura en gris (64x36) del fotograma con la anterior; si el cambio supera `SCENE_CHANGE_THRESHOLD = 0.12`, la escena entera se ha movido y el seguimiento se reinicia en vez de avisar. Calibrado con fotogramas reales: escena quieta 0.0001, un objeto grande moviéndose 0.019-0.062, y un desplazamiento del encuadre del 5% ya da 0.119.
|
|
|
|
Además se exige que la velocidad supere el umbral durante **3 fotogramas seguidos** (`SPEED_FRAMES_REQUIRED`), no en uno suelto.
|
|
|
|
> **Límite conocido:** un paneo **rápido y sostenido** de la cámara (arrastrarla mientras se mueve, sin parar) sigue pareciendo una carrera: el movimiento es continuo y coherente, así que ni el descarte de saltos ni la puerta de escena lo distinguen. La solución completa sería compensar el movimiento global con correlación de fase; no se ha implementado porque recolocar la cámara son movimientos bruscos, que sí se detectan.
|
|
|
|
---
|
|
7. Detección de Ladridos (audio de la cámara)
|
|
La cámara Tapo publica su micrófono dentro del mismo stream RTSP que el vídeo, así que **no hace falta micrófono USB**. El audio va en G.711 A-law, 8 kHz mono, pero el vídeo y el audio se piden en dos conexiones RTSP distintas (la cámara admite varias en paralelo), porque OpenCV no sabe decodificar sonido y hay que usar PyAV.
|
|
|
|
`pet_audio.py`:
|
|
- Remuestrea el audio a los 16 kHz que espera YAMNet y lo analiza en **ventanas de 0.975 s con medio solape**.
|
|
- Carga YAMNet en ONNX (`models/yamnet.onnx`, 16 MB, *no versionado: se descarga con `bash scripts/fetch_models.sh`*).
|
|
- Dispara por la clase **`Bark`** de AudioSet, no por el grupo canino: con el mismo material, `Bark` separa perros de gatos con 6 veces más margen, porque las clases genéricas (`Animal`, `Whimper`) también se activan con maullidos. El grupo canino se calcula solo como contexto en el aviso.
|
|
- Avisa como máximo una vez cada `BARK_COOLDOWN` segundos, para no inundar la consola con un ladrido sostenido.
|
|
- Se reconecta solo si la cámara se cae, y **nunca imprime la contraseña**: las excepciones de PyAV incluyen la URL completa con credenciales, así que todo el texto de error pasa por un enmascarador.
|
|
|
|
Calidad del audio, medida con [BarkMeowDB](https://zenodo.org/records/3563990) (CC-BY-4.0, 46 wavs de perro y 50 de gato) pasando el audio por el códec real de la cámara con `ffmpeg`:
|
|
|
|
| ruta | Bark en perros | Bark en gatos | disparan (umbral 0.15) |
|
|
|---|---|---|---|
|
|
| 16 kHz (referencia) | 0.715 | 0.020 | — |
|
|
| 8 kHz A-law (cámara) | 0.616 | 0.007 (máx 0.049) | 20/20 perros, 0/20 gatos |
|
|
|
|
El recorte de banda a 4 kHz cuesta un 14% de puntuación, pero la separación entre perro y gato sigue siendo enorme, así que el audio de la cámara sirve de sobra. Coste: **4.6 ms por ventana** en CPU, unas 200 veces más rápido que el tiempo real.
|
|
|
|
> **Cómo ajustar el umbral en tu casa:** la línea `[STATS]` muestra `Bark máx` de los últimos 10 s junto al umbral. Si en una semana de uso ves que se acerca al umbral sin que ladre nadie, súbelo con `BARK_THRESHOLD` en el `.env`. Si se le escapan ladridos, bájalo.
|
|
>
|
|
> **Pendiente de comprobar in situ:** el experimento aísla la pérdida de códec y banda, pero no la calidad del micrófono de la cámara, la distancia al animal ni la acústica de la sala. Conviene validarlo con un ladrido real delante de la cámara.
|
|
|
|
---
|
|
8. Avisos por Telegram
|
|
Cuando salta una alerta (carrera o ladrido), el monitor manda un mensaje **con la foto de la cámara anotada**: la caja de la mascota detectada, su confianza y la fecha y hora. Las capturas se guardan además en `capturas/` (ignorado por git).
|
|
|
|
Puesta en marcha (una sola vez):
|
|
1. En Telegram, habla con **@BotFather** y envía `/newbot`. Te devolverá un token.
|
|
2. **Escríbele cualquier mensaje a tu bot** (si no, el bot no puede ver tu chat).
|
|
3. Añade el token a `~/pet_monitor/.env`:
|
|
```
|
|
TELEGRAM_BOT_TOKEN=123456:ABC...
|
|
```
|
|
4. Comprueba que todo funciona:
|
|
```bash
|
|
uv run scripts/telegram_check.py
|
|
```
|
|
Ese script valida el token, **deduce el `chat_id` solo** a partir del último mensaje que recibió el bot, manda un mensaje y una foto de prueba, y te dice qué línea añadir al `.env` (`TELEGRAM_CHAT_ID`) para no tener que deducirlo cada vez.
|
|
|
|
Cómo se comporta:
|
|
- Los envíos van por una **cola con su propio hilo**: la red nunca bloquea ni la visión ni la escucha de audio. Si Telegram va lento, se encolan.
|
|
- Si falla, reintenta 3 veces con retroceso exponencial, y respeta el `retry_after` que pide Telegram ante un límite de frecuencia.
|
|
- Si la cola se llena (Telegram caído mucho rato), **descarta los avisos viejos** en favor de los nuevos: uno de hace cinco minutos ya no sirve.
|
|
- **Anti-repetición en dos niveles**: la consola agrupa avisos cada 10 s (antes un perro corriendo imprimía 14 líneas por segundo y tapaba las estadísticas) y las capturas/Telegram como mucho una cada `ALERT_COOLDOWN` (60 s por defecto) y por tipo de aviso.
|
|
- El **token nunca se imprime**: todo texto de error pasa por el enmascarador, así que en `journalctl` aparecería `.../bot***/sendMessage`.
|
|
|
|
Variables opcionales en el `.env`:
|
|
```
|
|
# TELEGRAM_CHAT_ID=123456789 # deducido solo si no se indica
|
|
# ALERT_COOLDOWN=60 # segundos entre capturas del mismo tipo de aviso
|
|
# SNAPSHOTS=0 # no guardar las capturas en disco
|
|
# SNAPSHOT_DIR=capturas # dónde guardarlas
|
|
```
|
|
|
|
---
|
|
9. Próximos Pasos (Hoja de Ruta)
|
|
Fase A: Integración de Audio (Ladridos y Sonidos) — *hecha*
|
|
Descartado el micrófono USB: se usa el de la propia cámara.
|
|
El modelo YAMNet ONNX y el diccionario de clases de AudioSet (`yamnet_classes.csv`) están en `models/`.
|
|
Listener en hilo secundario analizando ventanas de 0.975 s en busca de la clase `Bark`: implementado en `pet_audio.py`.
|
|
Fase B: Sistema de Alertas y Notificaciones — *hecha (falta poner el token)*
|
|
Bot de Telegram, despachador asíncrono con control de enfriamiento y captura JPEG con la caja delimitadora: implementado en `pet_telegram.py` y `pet_alerts.py`.
|
|
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:
|
|
```bash
|
|
sudo systemctl enable --now pet_monitor.service
|
|
```
|