02-ago: [proyecto] vigilancia - reconocimiento facial (YuNet+ArcFace) y corporal (ResNet18) + registro por webcam (enroll.py) + integracion en main.py
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ telegram_mensajes.log
|
|||||||
vigilancia/snapshots/
|
vigilancia/snapshots/
|
||||||
vigilancia/*.pt
|
vigilancia/*.pt
|
||||||
vigilancia/.venv/
|
vigilancia/.venv/
|
||||||
|
vigilancia/modelos/
|
||||||
|
vigilancia/conocidos/
|
||||||
|
|||||||
121
vigilancia/enroll.py
Normal file
121
vigilancia/enroll.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import reconocedor as rec
|
||||||
|
|
||||||
|
CONOCIDOS = rec.CONOCIDOS
|
||||||
|
WEB_CAM = 0
|
||||||
|
|
||||||
|
|
||||||
|
def escribir(texto, frame):
|
||||||
|
cv2.putText(frame, texto, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def capturar_cara(r, frame):
|
||||||
|
caras = r.detectar_caras(frame)
|
||||||
|
if not caras:
|
||||||
|
return None, None
|
||||||
|
mejor = max(caras, key=lambda c: c[-1])
|
||||||
|
emb = r.embedding_cara(frame, mejor)
|
||||||
|
return emb, mejor
|
||||||
|
|
||||||
|
|
||||||
|
def capturar_cuerpo(r, frame):
|
||||||
|
personas = r._detectar_personas(frame)
|
||||||
|
if not personas:
|
||||||
|
return None, None
|
||||||
|
mayor = max(personas, key=lambda p: p[2] * p[3])
|
||||||
|
x, y, w, h = mayor
|
||||||
|
recorte = frame[y:y + h, x:x + w]
|
||||||
|
emb = r.embedding_cuerpo(recorte)
|
||||||
|
return emb, mayor
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
nombres = sys.argv[1:3]
|
||||||
|
while len(nombres) < 2:
|
||||||
|
nombres.append(f"Hija{len(nombres) + 1}")
|
||||||
|
print(f"Registrando: {nombres}")
|
||||||
|
|
||||||
|
r = rec.cargar_reconocedor()
|
||||||
|
os.makedirs(CONOCIDOS, exist_ok=True)
|
||||||
|
|
||||||
|
cap = cv2.VideoCapture(WEB_CAM)
|
||||||
|
if not cap.isOpened():
|
||||||
|
print("ERROR: no se pudo abrir la webcam")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cv2.namedWindow("Registro", cv2.WINDOW_NORMAL)
|
||||||
|
|
||||||
|
for nombre in nombres:
|
||||||
|
carpeta = os.path.join(CONOCIDOS, nombre)
|
||||||
|
os.makedirs(carpeta, exist_ok=True)
|
||||||
|
print(f"\n>>> Hija: {nombre}")
|
||||||
|
|
||||||
|
fase = "cara"
|
||||||
|
while True:
|
||||||
|
ok, frame = cap.read()
|
||||||
|
if not ok:
|
||||||
|
continue
|
||||||
|
|
||||||
|
caras = r.detectar_caras(frame)
|
||||||
|
personas = r._detectar_personas(frame)
|
||||||
|
|
||||||
|
for c in caras:
|
||||||
|
x, y, w, h = (int(v) for v in c[:4])
|
||||||
|
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
|
||||||
|
for p in personas:
|
||||||
|
x, y, w, h = p
|
||||||
|
cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
|
||||||
|
|
||||||
|
if fase == "cara":
|
||||||
|
escribir("CARA: pon la cara mirando a la camara y pulsa ESPACIO", frame)
|
||||||
|
else:
|
||||||
|
escribir("CUERPO: alejate (cuerpo entero) y pulsa B", frame)
|
||||||
|
escribir(f"Persona: {nombre}", frame)
|
||||||
|
|
||||||
|
cv2.imshow("Registro", frame)
|
||||||
|
tecla = cv2.waitKey(1) & 0xFF
|
||||||
|
|
||||||
|
if tecla == ord("q"):
|
||||||
|
cap.release()
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
print("Abortado por el usuario")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if fase == "cara" and tecla == ord(" "):
|
||||||
|
emb, caja = capturar_cara(r, frame)
|
||||||
|
if emb is None:
|
||||||
|
print(" No se vio ninguna cara clara. Reintenta.")
|
||||||
|
continue
|
||||||
|
np.save(os.path.join(carpeta, "face.npy"), emb)
|
||||||
|
x, y, w, h = (int(v) for v in caja[:4])
|
||||||
|
cv2.imwrite(os.path.join(carpeta, "cara_ref.jpg"),
|
||||||
|
frame[max(0, y - 20):y + h + 20, max(0, x - 20):x + w + 20])
|
||||||
|
print(f" Cara de {nombre} registrada (score {caja[-1]:.2f})")
|
||||||
|
fase = "cuerpo"
|
||||||
|
|
||||||
|
elif fase == "cuerpo" and tecla == ord("b"):
|
||||||
|
emb, caja = capturar_cuerpo(r, frame)
|
||||||
|
if emb is None:
|
||||||
|
print(" No se detecto ninguna persona. Reintenta.")
|
||||||
|
continue
|
||||||
|
np.save(os.path.join(carpeta, "body.npy"), emb)
|
||||||
|
x, y, w, h = caja
|
||||||
|
cv2.imwrite(os.path.join(carpeta, "cuerpo_ref.jpg"),
|
||||||
|
frame[max(0, y):y + h, max(0, x):x + w])
|
||||||
|
print(f" Cuerpo de {nombre} registrado")
|
||||||
|
break
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
cv2.destroyAllWindows()
|
||||||
|
print("\nRegistro completado. Conocidos guardados:")
|
||||||
|
for n in nombres:
|
||||||
|
print(f" - {n} (face.npy + body.npy)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -9,10 +9,45 @@ from camara import CamaraRTSP
|
|||||||
from detector_movimiento import DetectorMovimiento
|
from detector_movimiento import DetectorMovimiento
|
||||||
from notificador import disparar_webhook_alexa, enviar_telegram
|
from notificador import disparar_webhook_alexa, enviar_telegram
|
||||||
|
|
||||||
USAR_YOLO = False
|
USAR_YOLO = True
|
||||||
|
identificador = None
|
||||||
|
|
||||||
if USAR_YOLO:
|
|
||||||
from detector_persona import DetectorPersona
|
def cargar_identificador():
|
||||||
|
global identificador
|
||||||
|
if identificador is None:
|
||||||
|
try:
|
||||||
|
from reconocedor import CONOCIDOS, Reconocedor, os as _os
|
||||||
|
if _os.path.isdir(CONOCIDOS) and any(
|
||||||
|
_os.path.isdir(_os.path.join(CONOCIDOS, n))
|
||||||
|
for n in _os.listdir(CONOCIDOS)
|
||||||
|
):
|
||||||
|
identificador = Reconocedor()
|
||||||
|
print(f"Identificacion activa. Conocidos: "
|
||||||
|
f"{list(identificador.conocidos.keys())}")
|
||||||
|
else:
|
||||||
|
print("Sin conocidos registrados (ejecuta enroll.py). "
|
||||||
|
"Modo deteccion simple.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"No se pudo cargar identificador: {e}")
|
||||||
|
return identificador
|
||||||
|
|
||||||
|
|
||||||
|
def componer_alerta(resultados, cam_nombre, ts):
|
||||||
|
conocidas = sorted({r["nombre"] for r in resultados if "nombre" in r})
|
||||||
|
desconocidas = sum(1 for r in resultados if r.get("desconocido"))
|
||||||
|
|
||||||
|
if conocidas and desconocidas:
|
||||||
|
return (f"👧 {', '.join(conocidas)} | "
|
||||||
|
f"⚠️ {desconocidas} persona(s) desconocida(s) "
|
||||||
|
f"en {cam_nombre} - {ts}")
|
||||||
|
if conocidas:
|
||||||
|
return (f"👧 {', '.join(conocidas)} detectada(s) "
|
||||||
|
f"en {cam_nombre} - {ts}")
|
||||||
|
if desconocidas:
|
||||||
|
return (f"⚠️ {desconocidas} persona(s) desconocida(s) "
|
||||||
|
f"en {cam_nombre} - {ts}")
|
||||||
|
return f"🚨 Movimiento en {cam_nombre} - {ts}"
|
||||||
|
|
||||||
|
|
||||||
def dentro_de_franja_horaria(hora_actual=None) -> bool:
|
def dentro_de_franja_horaria(hora_actual=None) -> bool:
|
||||||
@@ -37,9 +72,11 @@ def main():
|
|||||||
ultima_alerta = {}
|
ultima_alerta = {}
|
||||||
|
|
||||||
for i, c in enumerate(config.CAMARAS):
|
for i, c in enumerate(config.CAMARAS):
|
||||||
detectores.append(DetectorPersona() if USAR_YOLO else DetectorMovimiento())
|
detectores.append(DetectorMovimiento())
|
||||||
ultima_alerta[i] = 0
|
ultima_alerta[i] = 0
|
||||||
|
|
||||||
|
ident = cargar_identificador()
|
||||||
|
|
||||||
print("Sistema de vigilancia iniciado. Franja activa: "
|
print("Sistema de vigilancia iniciado. Franja activa: "
|
||||||
f"{config.HORA_INICIO}:00 - {config.HORA_FIN}:00")
|
f"{config.HORA_INICIO}:00 - {config.HORA_FIN}:00")
|
||||||
|
|
||||||
@@ -54,10 +91,7 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
detector = detectores[i]
|
detector = detectores[i]
|
||||||
detectado = (
|
detectado = detector.hay_movimiento(frame)
|
||||||
detector.hay_persona(frame) if USAR_YOLO
|
|
||||||
else detector.hay_movimiento(frame)
|
|
||||||
)
|
|
||||||
|
|
||||||
ahora = time.time()
|
ahora = time.time()
|
||||||
if detectado and (ahora - ultima_alerta[i] > config.COOLDOWN):
|
if detectado and (ahora - ultima_alerta[i] > config.COOLDOWN):
|
||||||
@@ -65,6 +99,16 @@ def main():
|
|||||||
ruta = os.path.join(
|
ruta = os.path.join(
|
||||||
config.SNAPSHOT_DIR, f"snapshot_{cam.nombre}_{ts}.jpg"
|
config.SNAPSHOT_DIR, f"snapshot_{cam.nombre}_{ts}.jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
texto = f"🚨 Movimiento en {cam.nombre} - {ts}"
|
||||||
|
if ident is not None:
|
||||||
|
try:
|
||||||
|
resultados = ident.analizar_frame(frame)
|
||||||
|
if resultados:
|
||||||
|
texto = componer_alerta(resultados, cam.nombre, ts)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{cam.nombre}] Error identificando: {e}")
|
||||||
|
|
||||||
cv2_ok = True
|
cv2_ok = True
|
||||||
try:
|
try:
|
||||||
cv2.imwrite(ruta, frame)
|
cv2.imwrite(ruta, frame)
|
||||||
@@ -72,9 +116,6 @@ def main():
|
|||||||
cv2_ok = False
|
cv2_ok = False
|
||||||
print(f"[{cam.nombre}] Error guardando snapshot: {e}")
|
print(f"[{cam.nombre}] Error guardando snapshot: {e}")
|
||||||
|
|
||||||
tipo = "persona" if USAR_YOLO else "movimiento"
|
|
||||||
texto = f"⚠️ Detección de {tipo} en {cam.nombre} - {ts}"
|
|
||||||
|
|
||||||
if cv2_ok:
|
if cv2_ok:
|
||||||
enviar_telegram(ruta, texto)
|
enviar_telegram(ruta, texto)
|
||||||
disparar_webhook_alexa(texto)
|
disparar_webhook_alexa(texto)
|
||||||
|
|||||||
184
vigilancia/reconocedor.py
Normal file
184
vigilancia/reconocedor.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torchvision.models as models
|
||||||
|
from torchvision import transforms
|
||||||
|
|
||||||
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
MODELOS = os.path.join(BASE, "modelos")
|
||||||
|
CONOCIDOS = os.path.join(BASE, "conocidos")
|
||||||
|
YUNET_PATH = os.path.join(MODELOS, "face_detection_yunet.onnx")
|
||||||
|
ARCFACE_PATH = os.path.join(MODELOS, "buffalo_s", "w600k_mbf.onnx")
|
||||||
|
YOLO_PATH = os.path.join(BASE, "yolov8n.pt")
|
||||||
|
|
||||||
|
UMBRAL_CARA = 0.40
|
||||||
|
UMBRAL_CUERPO = 0.55
|
||||||
|
|
||||||
|
|
||||||
|
class Reconocedor:
|
||||||
|
def __init__(self):
|
||||||
|
self.yunet = cv2.FaceDetectorYN_create(YUNET_PATH, "", (320, 320))
|
||||||
|
self.yunet.setScoreThreshold(0.6)
|
||||||
|
self.arcface = cv2.dnn.readNetFromONNX(ARCFACE_PATH)
|
||||||
|
self.body = self._cargar_body()
|
||||||
|
self.tf_body = transforms.Compose([
|
||||||
|
transforms.ToPILImage(),
|
||||||
|
transforms.Resize((224, 224)),
|
||||||
|
transforms.ToTensor(),
|
||||||
|
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
|
||||||
|
])
|
||||||
|
self.conocidos = self._cargar_conocidos()
|
||||||
|
|
||||||
|
def _cargar_body(self):
|
||||||
|
m = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
|
||||||
|
m.fc = torch.nn.Identity()
|
||||||
|
m.eval()
|
||||||
|
return m
|
||||||
|
|
||||||
|
def _cargar_conocidos(self):
|
||||||
|
conocidos = {}
|
||||||
|
if not os.path.isdir(CONOCIDOS):
|
||||||
|
return conocidos
|
||||||
|
for nombre in os.listdir(CONOCIDOS):
|
||||||
|
carpeta = os.path.join(CONOCIDOS, nombre)
|
||||||
|
if not os.path.isdir(carpeta):
|
||||||
|
continue
|
||||||
|
emb_cara = self._cargar_npy(carpeta, "face")
|
||||||
|
emb_cuerpo = self._cargar_npy(carpeta, "body")
|
||||||
|
if emb_cara is not None or emb_cuerpo is not None:
|
||||||
|
conocidos[nombre] = (emb_cara, emb_cuerpo)
|
||||||
|
return conocidos
|
||||||
|
|
||||||
|
def _cargar_npy(self, carpeta, tipo):
|
||||||
|
ruta = os.path.join(carpeta, f"{tipo}.npy")
|
||||||
|
if os.path.exists(ruta):
|
||||||
|
return np.load(ruta)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Cara
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def detectar_caras(self, frame):
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
self.yunet.setInputSize((w, h))
|
||||||
|
_, caras = self.yunet.detect(frame)
|
||||||
|
if caras is None:
|
||||||
|
return []
|
||||||
|
return [c for c in caras if c[-1] >= 0.6]
|
||||||
|
|
||||||
|
def embedding_cara(self, frame, caja):
|
||||||
|
x, y, w, h = (int(v) for v in caja[:4])
|
||||||
|
x, y = max(0, x), max(0, y)
|
||||||
|
recorte = frame[y:y + h, x:x + w]
|
||||||
|
if recorte.size == 0:
|
||||||
|
return None
|
||||||
|
blob = cv2.dnn.blobFromImage(
|
||||||
|
recorte, 1.0 / 128, (112, 112), (127.5, 127.5, 127.5), swapRB=True
|
||||||
|
)
|
||||||
|
self.arcface.setInput(blob)
|
||||||
|
emb = self.arcface.forward().flatten()
|
||||||
|
norm = np.linalg.norm(emb)
|
||||||
|
return emb / norm if norm > 0 else None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Cuerpo
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def embedding_cuerpo(self, recorte_bgr):
|
||||||
|
if recorte_bgr.size == 0:
|
||||||
|
return None
|
||||||
|
rgb = cv2.cvtColor(recorte_bgr, cv2.COLOR_BGR2RGB)
|
||||||
|
tensor = self.tf_body(rgb).unsqueeze(0)
|
||||||
|
with torch.no_grad():
|
||||||
|
emb = self.body(tensor).numpy().flatten()
|
||||||
|
norm = np.linalg.norm(emb)
|
||||||
|
return emb / norm if norm > 0 else None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Identificacion
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def identificar_cara(self, emb):
|
||||||
|
if emb is None or not self.conocidos:
|
||||||
|
return None, 0.0
|
||||||
|
mejor, score = None, -1.0
|
||||||
|
for nombre, (emb_cara, _) in self.conocidos.items():
|
||||||
|
if emb_cara is not None:
|
||||||
|
sim = float(np.dot(emb, emb_cara))
|
||||||
|
if sim > score:
|
||||||
|
score = sim
|
||||||
|
mejor = nombre
|
||||||
|
return (mejor, score) if score >= UMBRAL_CARA else (None, score)
|
||||||
|
|
||||||
|
def identificar_cuerpo(self, emb):
|
||||||
|
if emb is None or not self.conocidos:
|
||||||
|
return None, 0.0
|
||||||
|
mejor, score = None, -1.0
|
||||||
|
for nombre, (_, emb_cuerpo) in self.conocidos.items():
|
||||||
|
if emb_cuerpo is not None:
|
||||||
|
sim = float(np.dot(emb, emb_cuerpo))
|
||||||
|
if sim > score:
|
||||||
|
score = sim
|
||||||
|
mejor = nombre
|
||||||
|
return (mejor, score) if score >= UMBRAL_CUERPO else (None, score)
|
||||||
|
|
||||||
|
def analizar_frame(self, frame):
|
||||||
|
"""Devuelve [{nombre:'X'} | {'desconocido':True}] por cada persona del frame."""
|
||||||
|
personas = self._detectar_personas(frame)
|
||||||
|
if not personas:
|
||||||
|
return []
|
||||||
|
resultados = []
|
||||||
|
for p in personas:
|
||||||
|
x, y, w, h = p
|
||||||
|
persona = self._identificar_recorte(frame, x, y, w, h)
|
||||||
|
resultados.append(persona)
|
||||||
|
return resultados
|
||||||
|
|
||||||
|
def _detectar_personas(self, frame):
|
||||||
|
try:
|
||||||
|
from ultralytics import YOLO
|
||||||
|
model = self._modelo_yolo()
|
||||||
|
r = model.predict(frame, verbose=False, conf=0.4)
|
||||||
|
cajas = []
|
||||||
|
for box in r[0].boxes:
|
||||||
|
if int(box.cls) == 0:
|
||||||
|
x1, y1, x2, y2 = box.xyxy[0].tolist()
|
||||||
|
cajas.append([int(x1), int(y1), int(x2 - x1), int(y2 - y1)])
|
||||||
|
return cajas
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _modelo_yolo(self):
|
||||||
|
if not hasattr(self, "_yolo"):
|
||||||
|
from ultralytics import YOLO
|
||||||
|
self._yolo = YOLO(YOLO_PATH)
|
||||||
|
return self._yolo
|
||||||
|
|
||||||
|
def _identificar_recorte(self, frame, x, y, w, h):
|
||||||
|
x, y = max(0, x), max(0, y)
|
||||||
|
recorte = frame[y:y + h, x:x + w]
|
||||||
|
if recorte.size == 0:
|
||||||
|
return {"desconocido": True}
|
||||||
|
|
||||||
|
mejor_nombre, mejor_score = None, 0.0
|
||||||
|
for cara in self.detectar_caras(frame):
|
||||||
|
cx, cy, cw, ch = (int(v) for v in cara[:4])
|
||||||
|
if cx < x + w and cx + cw > x and cy < y + h and cy + ch > y:
|
||||||
|
emb = self.embedding_cara(frame, cara)
|
||||||
|
nombre, score = self.identificar_cara(emb)
|
||||||
|
if nombre and score > mejor_score:
|
||||||
|
mejor_nombre, mejor_score = nombre, score
|
||||||
|
|
||||||
|
if mejor_nombre:
|
||||||
|
return {"nombre": mejor_nombre, "score": mejor_score}
|
||||||
|
|
||||||
|
emb_cuerpo = self.embedding_cuerpo(recorte)
|
||||||
|
nombre, score = self.identificar_cuerpo(emb_cuerpo)
|
||||||
|
if nombre:
|
||||||
|
return {"nombre": nombre, "score": score, "via": "cuerpo"}
|
||||||
|
|
||||||
|
return {"desconocido": True}
|
||||||
|
|
||||||
|
|
||||||
|
def cargar_reconocedor():
|
||||||
|
return Reconocedor()
|
||||||
Reference in New Issue
Block a user