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).
This commit is contained in:
parent
2fbace4109
commit
42ff966f5a
3 changed files with 200 additions and 39 deletions
27
Readme.md
27
Readme.md
|
|
@ -75,6 +75,7 @@ uv pip install sounddevice numpy scipy requests onnxruntime
|
||||||
El proyecto se reparte en módulos, más los scripts de apoyo:
|
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_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_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_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_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).
|
- `pet_telegram.py`: cliente de Telegram (cola + hilo de envío, reintentos).
|
||||||
|
|
@ -87,7 +88,7 @@ El proyecto se reparte en módulos, más los scripts de apoyo:
|
||||||
- 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.
|
- 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`).
|
- 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).
|
- 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).
|
- 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).
|
- 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.
|
- Libera el stream RTSP y el acelerador Hailo al recibir `SIGINT` (`Ctrl+C`) o `SIGTERM`, de cara a ejecutarlo como servicio systemd.
|
||||||
|
|
||||||
|
|
@ -103,7 +104,25 @@ Ejecución:
|
||||||
uv run pet_speed.py
|
uv run pet_speed.py
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
6. Detección de Ladridos (audio de la cámara)
|
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.
|
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`:
|
`pet_audio.py`:
|
||||||
|
|
@ -127,7 +146,7 @@ El recorte de banda a 4 kHz cuesta un 14% de puntuación, pero la separación en
|
||||||
> **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.
|
> **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
7. Avisos por Telegram
|
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).
|
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):
|
Puesta en marcha (una sola vez):
|
||||||
|
|
@ -159,7 +178,7 @@ Variables opcionales en el `.env`:
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
8. Próximos Pasos (Hoja de Ruta)
|
9. Próximos Pasos (Hoja de Ruta)
|
||||||
Fase A: Integración de Audio (Ladridos y Sonidos) — *hecha*
|
Fase A: Integración de Audio (Ladridos y Sonidos) — *hecha*
|
||||||
Descartado el micrófono USB: se usa el de la propia cámara.
|
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/`.
|
El modelo YAMNet ONNX y el diccionario de clases de AudioSet (`yamnet_classes.csv`) están en `models/`.
|
||||||
|
|
|
||||||
116
pet_motion.py
Normal file
116
pet_motion.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
Seguimiento del movimiento de la mascota principal.
|
||||||
|
|
||||||
|
El cálculo original comparaba el centroide entre dos fotogramas consecutivos, y
|
||||||
|
nada más. Con la Tapo a 15 fps eso da falsos positivos por tres vías distintas:
|
||||||
|
|
||||||
|
1. Mover la cámara: el detector encuentra "algo" antes y después del cambio de
|
||||||
|
encuadre, y el salto entre ambas posiciones parece una carrera. Es el caso
|
||||||
|
real que se dio el 2026-10-01.
|
||||||
|
2. Parpadeo del detector: la caja se desplaza unos píxeles de un fotograma al
|
||||||
|
siguiente sin que el animal se mueva.
|
||||||
|
3. Cambio de sujeto: si hay dos animales, o el detector duda, el centroide
|
||||||
|
salta de uno a otro.
|
||||||
|
|
||||||
|
Medir la velocidad fotograma a fotograma es muy ruidoso: con la caja temblando
|
||||||
|
+-2% del encuadre, la velocidad instantánea llega a 0.8 norm/s sin que nadie se
|
||||||
|
mueva. Por eso aquí la velocidad se mide como el **desplazamiento neto sobre una
|
||||||
|
ventana de tiempo** (0.2 s por defecto): el temblor se cancela, el movimiento
|
||||||
|
real no.
|
||||||
|
|
||||||
|
Salvaguardas:
|
||||||
|
- Asociación con la detección más cercana a la anterior, no con la primera.
|
||||||
|
- Descarte de saltos imposibles para un animal (cámara movida, cambio de sujeto).
|
||||||
|
- Velocidad medida sobre una ventana, no entre dos fotogramas.
|
||||||
|
- Exigencia de varios fotogramas seguidos por encima del umbral.
|
||||||
|
- La escena puede reiniciar el seguimiento cuando cambia entera (lo detecta
|
||||||
|
`pet_speed.py` comparando miniaturas de fotograma).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
# Detección ya convertida a píxeles (caja) y a coordenadas normalizadas (centro)
|
||||||
|
Deteccion = namedtuple("Deteccion", "etiqueta confianza caja cx cy")
|
||||||
|
|
||||||
|
|
||||||
|
class MotionTracker:
|
||||||
|
"""Decide si la mascota seguida está corriendo."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
speed_threshold=0.30,
|
||||||
|
frames_required=3,
|
||||||
|
window=0.2,
|
||||||
|
max_plausible_speed=3.0,
|
||||||
|
max_history=10,
|
||||||
|
):
|
||||||
|
self.speed_threshold = speed_threshold
|
||||||
|
self.frames_required = frames_required
|
||||||
|
self.window = window
|
||||||
|
self.max_plausible_speed = max_plausible_speed
|
||||||
|
self.max_history = max_history
|
||||||
|
self.discarded = 0 # saltos imposibles descartados
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Olvida el seguimiento actual (cambio de escena, o pérdida del sujeto)."""
|
||||||
|
self._historial = []
|
||||||
|
self._racha = 0
|
||||||
|
self.speed = 0.0
|
||||||
|
|
||||||
|
def update(self, detecciones, timestamp):
|
||||||
|
"""
|
||||||
|
detecciones: lista de Deteccion del fotograma actual (puede ir vacía).
|
||||||
|
Devuelve (deteccion_elegida, velocidad, corriendo) o None si no hay nada.
|
||||||
|
"""
|
||||||
|
elegida = self._asocia(detecciones)
|
||||||
|
if elegida is None:
|
||||||
|
self.reset()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ¿Salto imposible desde el fotograma anterior?
|
||||||
|
if self._historial:
|
||||||
|
t_prev, x_prev, y_prev = self._historial[-1]
|
||||||
|
dt_prev = timestamp - t_prev
|
||||||
|
if dt_prev > 0 and math.hypot(elegida.cx - x_prev, elegida.cy - y_prev) / dt_prev > self.max_plausible_speed:
|
||||||
|
# Ningún animal cruza el encuadre varias veces por segundo: esto
|
||||||
|
# es la cámara moviéndose o el detector saltando a otro objeto.
|
||||||
|
self.discarded += 1
|
||||||
|
self._historial = [(timestamp, elegida.cx, elegida.cy)]
|
||||||
|
self._racha = 0
|
||||||
|
self.speed = 0.0
|
||||||
|
return elegida, 0.0, False
|
||||||
|
|
||||||
|
self._historial.append((timestamp, elegida.cx, elegida.cy))
|
||||||
|
while len(self._historial) > 2 and timestamp - self._historial[0][0] > self.window:
|
||||||
|
self._historial.pop(0)
|
||||||
|
if len(self._historial) > self.max_history:
|
||||||
|
self._historial.pop(0)
|
||||||
|
|
||||||
|
# Velocidad = desplazamiento neto sobre la ventana disponible
|
||||||
|
t0, x0, y0 = self._historial[0]
|
||||||
|
lapso = timestamp - t0
|
||||||
|
if len(self._historial) < 2 or lapso < self.window * 0.5:
|
||||||
|
# Todavía no hay ventana suficiente para juzgar
|
||||||
|
self.speed = 0.0
|
||||||
|
return elegida, 0.0, False
|
||||||
|
|
||||||
|
self.speed = math.hypot(elegida.cx - x0, elegida.cy - y0) / lapso
|
||||||
|
|
||||||
|
if self.speed > self.speed_threshold:
|
||||||
|
self._racha += 1
|
||||||
|
else:
|
||||||
|
self._racha = 0
|
||||||
|
|
||||||
|
return elegida, self.speed, self._racha >= self.frames_required
|
||||||
|
|
||||||
|
def _asocia(self, detecciones):
|
||||||
|
"""Se queda con la detección más parecida a la que venía siguiendo."""
|
||||||
|
if not detecciones:
|
||||||
|
return None
|
||||||
|
if not self._historial:
|
||||||
|
# Sin historial, la más fiable
|
||||||
|
return max(detecciones, key=lambda d: d.confianza)
|
||||||
|
_, cx, cy = self._historial[-1]
|
||||||
|
return min(detecciones, key=lambda d: math.hypot(d.cx - cx, d.cy - cy))
|
||||||
96
pet_speed.py
96
pet_speed.py
|
|
@ -4,8 +4,10 @@ Pet Monitor - Visión por computador con cámara IP Tapo (RTSP) + NPU Hailo.
|
||||||
|
|
||||||
- Ingesta RTSP de vídeo en un hilo aparte, que descarta los fotogramas viejos
|
- Ingesta RTSP de vídeo en un hilo aparte, que descarta los fotogramas viejos
|
||||||
y expone siempre el más reciente (evita la latencia acumulada típica de RTSP).
|
y expone siempre el más reciente (evita la latencia acumulada típica de RTSP).
|
||||||
- Detección de gato/perro (clases COCO 15 y 16) con YOLOv8s sobre la NPU Hailo
|
- Detección de gato/perro (clases COCO 15 y 16) con YOLOv8s sobre la NPU Hailo.
|
||||||
y alerta de carrera según el desplazamiento del centroide entre fotogramas.
|
- Alerta de carrera con seguimiento temporal (ver pet_motion.py): velocidad
|
||||||
|
medida sobre una ventana, descarte de saltos imposibles y reinicio del
|
||||||
|
seguimiento cuando la escena cambia entera porque han movido la cámara.
|
||||||
- Escucha de ladridos sobre el audio de la propia cámara (ver pet_audio.py),
|
- Escucha de ladridos sobre el audio de la propia cámara (ver pet_audio.py),
|
||||||
en un segundo hilo y con su propia conexión RTSP.
|
en un segundo hilo y con su propia conexión RTSP.
|
||||||
- Avisos por consola y a Telegram con captura anotada (ver pet_alerts.py).
|
- Avisos por consola y a Telegram con captura anotada (ver pet_alerts.py).
|
||||||
|
|
@ -38,6 +40,7 @@ from picamera2.devices.hailo import Hailo, hailo_architecture
|
||||||
from pet_alerts import AlertDispatcher
|
from pet_alerts import AlertDispatcher
|
||||||
from pet_audio import AudioListener, BarkDetector
|
from pet_audio import AudioListener, BarkDetector
|
||||||
from pet_common import build_rtsp_url, load_env, mask_url
|
from pet_common import build_rtsp_url, load_env, mask_url
|
||||||
|
from pet_motion import Deteccion, MotionTracker
|
||||||
from pet_telegram import TelegramNotifier
|
from pet_telegram import TelegramNotifier
|
||||||
|
|
||||||
# --- Configuración -------------------------------------------------------
|
# --- Configuración -------------------------------------------------------
|
||||||
|
|
@ -45,6 +48,11 @@ from pet_telegram import TelegramNotifier
|
||||||
TARGET_CLASSES = {15: "Gato", 16: "Perro"}
|
TARGET_CLASSES = {15: "Gato", 16: "Perro"}
|
||||||
CONF_THRESHOLD = 0.45 # Confianza mínima para considerar una detección
|
CONF_THRESHOLD = 0.45 # Confianza mínima para considerar una detección
|
||||||
SPEED_THRESHOLD = 0.30 # Velocidad normalizada por segundo (fracción del frame)
|
SPEED_THRESHOLD = 0.30 # Velocidad normalizada por segundo (fracción del frame)
|
||||||
|
SPEED_FRAMES_REQUIRED = 3 # Fotogramas seguidos por encima del umbral para avisar
|
||||||
|
SPEED_WINDOW = 0.2 # Ventana (s) sobre la que se mide la velocidad
|
||||||
|
MAX_PLAUSIBLE_SPEED = 3.0 # Salto imposible para un animal: cámara movida o cambio de sujeto
|
||||||
|
SCENE_CHANGE_THRESHOLD = 0.12 # Cambio global de la miniatura que delata que se movió la cámara
|
||||||
|
SCENE_THUMBNAIL = (64, 36) # Tamaño de la miniatura para detectar el cambio de escena
|
||||||
DEFAULT_STREAM = "stream2" # stream1 = 2304x1296@15, stream2 = 1280x720@15
|
DEFAULT_STREAM = "stream2" # stream1 = 2304x1296@15, stream2 = 1280x720@15
|
||||||
STATS_INTERVAL = 10.0 # Segundos entre líneas de estadísticas
|
STATS_INTERVAL = 10.0 # Segundos entre líneas de estadísticas
|
||||||
|
|
||||||
|
|
@ -318,9 +326,15 @@ def main():
|
||||||
signal.signal(signal.SIGINT, handle_signal)
|
signal.signal(signal.SIGINT, handle_signal)
|
||||||
signal.signal(signal.SIGTERM, handle_signal)
|
signal.signal(signal.SIGTERM, handle_signal)
|
||||||
|
|
||||||
|
tracker = MotionTracker(
|
||||||
|
speed_threshold=SPEED_THRESHOLD,
|
||||||
|
frames_required=SPEED_FRAMES_REQUIRED,
|
||||||
|
window=SPEED_WINDOW,
|
||||||
|
max_plausible_speed=MAX_PLAUSIBLE_SPEED,
|
||||||
|
)
|
||||||
last_seq = 0
|
last_seq = 0
|
||||||
last_centroid = None
|
miniatura_previa = None
|
||||||
last_frame_time = 0.0
|
escenas_cambiadas = 0
|
||||||
last_stats = time.time()
|
last_stats = time.time()
|
||||||
frames_processed = 0
|
frames_processed = 0
|
||||||
inference_time = 0.0
|
inference_time = 0.0
|
||||||
|
|
@ -344,9 +358,23 @@ def main():
|
||||||
frames_processed += 1
|
frames_processed += 1
|
||||||
|
|
||||||
src_h, src_w = frame.shape[:2]
|
src_h, src_w = frame.shape[:2]
|
||||||
dt = frame_time - last_frame_time if last_frame_time else 0.0
|
|
||||||
|
|
||||||
pet_found = False
|
# ¿Ha movido alguien la cámara? Si la escena cambia entera, no es la
|
||||||
|
# mascota moviéndose: reiniciamos el seguimiento en vez de avisar.
|
||||||
|
miniatura = cv2.cvtColor(
|
||||||
|
cv2.resize(frame, SCENE_THUMBNAIL, interpolation=cv2.INTER_AREA),
|
||||||
|
cv2.COLOR_BGR2GRAY,
|
||||||
|
)
|
||||||
|
if miniatura_previa is not None:
|
||||||
|
cambio = float(np.mean(cv2.absdiff(miniatura, miniatura_previa))) / 255.0
|
||||||
|
if cambio > SCENE_CHANGE_THRESHOLD:
|
||||||
|
tracker.reset()
|
||||||
|
escenas_cambiadas += 1
|
||||||
|
miniatura_previa = miniatura
|
||||||
|
|
||||||
|
# Todas las detecciones de interés del fotograma, en píxeles y en
|
||||||
|
# coordenadas normalizadas. El tracker decide con cuál se queda.
|
||||||
|
detecciones = []
|
||||||
for class_id, label in TARGET_CLASSES.items():
|
for class_id, label in TARGET_CLASSES.items():
|
||||||
if class_id >= len(detections_by_class):
|
if class_id >= len(detections_by_class):
|
||||||
continue
|
continue
|
||||||
|
|
@ -354,38 +382,35 @@ def main():
|
||||||
confidence = det[4]
|
confidence = det[4]
|
||||||
if confidence <= CONF_THRESHOLD:
|
if confidence <= CONF_THRESHOLD:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pet_found = True
|
|
||||||
x1, y1, x2, y2 = box_to_source(det, scale, pad_x, pad_y, src_w, src_h)
|
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)
|
detecciones.append(
|
||||||
|
Deteccion(
|
||||||
|
label,
|
||||||
|
float(confidence),
|
||||||
|
(x1, y1, x2, y2),
|
||||||
|
(x1 + x2) / 2.0 / src_w,
|
||||||
|
(y1 + y2) / 2.0 / src_h,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Cajas del fotograma actual, para anotar la captura del aviso
|
resultado = tracker.update(detecciones, frame_time)
|
||||||
alerts.set_pet_boxes([(label, float(confidence), (x1, y1, x2, y2))])
|
if resultado is None:
|
||||||
|
|
||||||
if last_centroid and dt > 0:
|
|
||||||
dist = math.hypot(centroid[0] - last_centroid[0], centroid[1] - last_centroid[1])
|
|
||||||
speed = dist / dt
|
|
||||||
if speed > SPEED_THRESHOLD:
|
|
||||||
alerts.alert(
|
|
||||||
"carrera",
|
|
||||||
f"⚠️ [ALERTA] ¡{label} corriendo! Vel: {speed:.2f} norm/s (Conf: {confidence:.2f})",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
alerts.alert(
|
|
||||||
"calmado",
|
|
||||||
f"🐾 [OK] {label} detectado (calmado). Vel: {speed:.2f} norm/s (Conf: {confidence:.2f})",
|
|
||||||
)
|
|
||||||
|
|
||||||
last_centroid = centroid
|
|
||||||
break # Evaluamos la mascota principal detectada
|
|
||||||
if pet_found:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not pet_found:
|
|
||||||
last_centroid = None
|
|
||||||
alerts.set_pet_boxes([])
|
alerts.set_pet_boxes([])
|
||||||
|
else:
|
||||||
last_frame_time = frame_time
|
elegida, velocidad, corriendo = resultado
|
||||||
|
alerts.set_pet_boxes([(elegida.etiqueta, elegida.confianza, elegida.caja)])
|
||||||
|
if corriendo:
|
||||||
|
alerts.alert(
|
||||||
|
"carrera",
|
||||||
|
f"⚠️ [ALERTA] ¡{elegida.etiqueta} corriendo! Vel: {velocidad:.2f} norm/s "
|
||||||
|
f"(Conf: {elegida.confianza:.2f})",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
alerts.alert(
|
||||||
|
"calmado",
|
||||||
|
f"🐾 [OK] {elegida.etiqueta} detectado (calmado). Vel: {velocidad:.2f} norm/s "
|
||||||
|
f"(Conf: {elegida.confianza:.2f})",
|
||||||
|
)
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now - last_stats >= STATS_INTERVAL:
|
if now - last_stats >= STATS_INTERVAL:
|
||||||
|
|
@ -400,6 +425,7 @@ def main():
|
||||||
f" || audio: Bark máx {audio.take_max_bark():.2f} (umbral {audio.threshold:.2f}) "
|
f" || audio: Bark máx {audio.take_max_bark():.2f} (umbral {audio.threshold:.2f}) "
|
||||||
f"| ventanas {audio.windows} | ladridos {audio.alerts}"
|
f"| ventanas {audio.windows} | ladridos {audio.alerts}"
|
||||||
)
|
)
|
||||||
|
linea += f" || escenas {escenas_cambiadas} | saltos descartados {tracker.discarded}"
|
||||||
linea += f" || {alerts.stats()}"
|
linea += f" || {alerts.stats()}"
|
||||||
print(linea)
|
print(linea)
|
||||||
last_stats = now
|
last_stats = now
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue