Pet-Monitor/Readme.md
abraham 42ff966f5a Evitar falsos positivos de carrera al mover la camara
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).
2026-10-01 18:51:37 +02:00

14 KiB

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.


  1. 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:
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:

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.


  1. 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

  1. 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:
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 ....


  1. 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:

uv run pet_speed.py

  1. 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:

  2. 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.

  3. 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).

  4. 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.


  1. 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 (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.


  1. 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:
    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

  1. 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:
   sudo systemctl enable --now pet_monitor.service