Detección facial en tiempo real con YuNet de OpenCV
Usa el detector YuNet de OpenCV para dibujar recuadros faciales y cinco puntos de referencia faciales en una vista previa de video. El ejemplo de Python que aparece a continuación procesa un archivo de video local o, de forma opcional, una cámara web, mantiene las detecciones asociadas a su fotograma de origen y libera la captura cuando lo detienes.
¿Por qué detectar rostros en tiempo real?
Una vista previa en streaming te permite observar cómo cambian las detecciones cuando los rostros se mueven, desaparecen o se vuelven más pequeños. YuNet localiza rostros; no reconoce personas ni sigue a una persona entre fotogramas. Que el bucle pueda mantener el ritmo de una transmisión en vivo depende de tu hardware y de la resolución de entrada.
Configura tu entorno
Esta receta de Bash se probó en Linux con Python 3.11.2,
opencv-python 4.13.0.92 con interfaz gráfica y NumPy 2.2.6. Son versiones probadas,
no requisitos mínimos intrínsecos. Necesitas tener instalado python3 con
venv y pip, y disponer de una pantalla de escritorio
funcional. Otros sistemas operativos quedan fuera del alcance probado de esta receta.
Las versiones fijadas mantienen deliberadamente el modelo de marzo de 2023 con OpenCV 4.x. OpenCV 5 está disponible, pero su motor ONNX Runtime necesita la exportación más reciente de YuNet con dimensiones dinámicas para tamaños de imagen variables; consulta el repositorio del modelo YuNet. No incorpores esa actualización a este ejemplo sin probar juntos el modelo y el entorno de ejecución.
Pega este bloque desde el directorio donde quieras crear yunet-stream. Se ejecuta
en un subshell, por lo que tu shell permanece en su directorio original tanto si termina
correctamente como si falla. El bloque rechaza un proyecto existente.
(
set -eu
command -v python3 >/dev/null || { printf '%s\n' 'Install Python with venv and pip first.' >&2; exit 1; }
mkdir yunet-stream &&
cd yunet-stream &&
python3 -m venv .venv &&
.venv/bin/python -m pip install 'opencv-python==4.13.0.92' 'numpy==2.2.6' &&
.venv/bin/python - <<'PY'
from hashlib import sha256
from pathlib import Path
from urllib.request import urlopen
revision = '47534e27c9851bb1128ccc0102f1145e27f23f98'
filename = 'face_detection_yunet_2023mar.onnx'
url = f'https://media.githubusercontent.com/media/opencv/opencv_zoo/{revision}/models/face_detection_yunet/{filename}'
with urlopen(url, timeout=60) as response:
data = response.read()
expected = '8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4'
if sha256(data).hexdigest() != expected:
raise SystemExit('Model checksum mismatch; do not run this download.')
Path(filename).write_bytes(data)
PY
)
Si la configuración falla, inspecciona el directorio parcial y elige un nombre de proyecto nuevo
antes de repetirla, cambiando tanto mkdir como
cd. El bloque no elimina archivos existentes. El nuevo entorno virtual
mantiene otras instalaciones de OpenCV fuera de esta receta: la
guía del paquete de OpenCV para Python exige elegir
una distribución, y los paquetes sin interfaz gráfica no pueden ofrecer esta ventana de vista
previa. La descarga del modelo tiene un tiempo de espera del socket de 60 segundos; si se agota
ese tiempo o falla la suma de verificación, la configuración se detiene antes de publicar el
archivo del modelo. Comprueba tu conexión antes de volver a intentarlo en un directorio nuevo.
Crea un pipeline básico de detección facial en streaming
Guarda este programa completo como yunet-stream/stream.py. Usa un solo detector de forma
secuencial y limita el procesamiento de OpenCV a un hilo. La decodificación de video y la interfaz
gráfica pueden tener sus propios hilos; la aplicación no tiene una cola de trabajadores.
import argparse
import sys
from time import perf_counter
import cv2
import numpy as np
def draw_faces(frame, faces):
if faces is None:
return
for face in faces:
x, y, width, height = face[:4].astype(np.int32)
cv2.rectangle(frame, (x, y), (x + width, y + height), (0, 255, 0), 2)
for point in face[4:14].astype(np.int32).reshape(-1, 2):
cv2.circle(frame, tuple(point), 2, (0, 255, 255), -1)
def run(args):
cv2.setNumThreads(1)
capture = None
displayed = inferred = 0
inference_seconds = loop_seconds = 0.0
source = args.video if args.video is not None else args.camera
try:
detector = cv2.FaceDetectorYN.create(
args.model, '', (320, 320), args.score, args.nms, 5000
)
capture = cv2.VideoCapture(source)
if not capture.isOpened():
raise RuntimeError(f'Could not open capture: {source!r}')
while True:
started = perf_counter()
valid, frame = capture.read()
if not valid:
if displayed == 0 or args.camera is not None:
raise RuntimeError(f'Capture stopped without a usable frame: {source!r}')
print('File capture ended; EOF and decode failure share this signal.')
break
if frame is None or frame.size == 0:
raise RuntimeError(f'Capture returned an empty frame: {source!r}')
# Results belong only to this decoded frame, including skipped frames.
faces = None
if displayed % args.every == 0:
height, width = frame.shape[:2]
small_width = round(width * args.scale)
small_height = round(height * args.scale)
if min(small_width, small_height) < 32:
raise RuntimeError('Scaled frames must be at least 32 by 32 pixels.')
small = cv2.resize(frame, (small_width, small_height))
detector.setInputSize((small_width, small_height))
inference_started = perf_counter()
_, faces = detector.detect(small)
inference_seconds += perf_counter() - inference_started
inferred += 1
if faces is not None:
faces[:, 0:14:2] *= width / small_width
faces[:, 1:14:2] *= height / small_height
draw_faces(frame, faces)
cv2.imshow('YuNet stream', frame)
key = cv2.waitKey(1) & 0xFF
displayed += 1
loop_seconds += perf_counter() - started
if key == ord('q'):
break
finally:
if capture is not None:
capture.release()
cv2.destroyAllWindows()
print(f'Frames displayed: {displayed}; frames inferred: {inferred}')
if inferred:
print(f'Mean detect() time: {1000 * inference_seconds / inferred:.2f} ms')
if loop_seconds > 0:
print(f'Capture-to-display loop: {displayed / loop_seconds:.2f} frames/s')
if args.video is not None:
print('File feed is unpaced; these timings do not prove webcam performance or accuracy.')
def main():
parser = argparse.ArgumentParser(description='Preview YuNet face detections.')
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument('--video', help='Local video filename, not a camera index.')
source.add_argument('--camera', type=int, help='Camera device index.')
parser.add_argument('--model', default='face_detection_yunet_2023mar.onnx')
parser.add_argument('--scale', type=float, default=1.0)
parser.add_argument('--every', type=int, default=1)
parser.add_argument('--score', type=float, default=0.9)
parser.add_argument('--nms', type=float, default=0.3)
args = parser.parse_args()
if not 0 < args.scale <= 1 or args.every < 1:
parser.error('--scale must be in (0, 1] and --every must be positive.')
if not 0 < args.score <= 1 or not 0 < args.nms <= 1:
parser.error('--score and --nms must be in (0, 1].')
if args.camera is not None and args.camera < 0:
parser.error('--camera must be nonnegative.')
run(args)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('Stopped by Ctrl+C.', file=sys.stderr)
sys.exit(130)
except cv2.error:
print('OpenCV failed. Check the model, decoded frames, and display.', file=sys.stderr)
sys.exit(1)
except RuntimeError as error:
print(str(error), file=sys.stderr)
sys.exit(1)
Coloca un video local que OpenCV pueda decodificar en yunet-stream/input.mkv y luego pega
esto desde el directorio superior. Un nombre de archivo que contiene espacios sigue siendo un
único argumento entre comillas.
(
cd yunet-stream &&
.venv/bin/python stream.py --video './input.mkv'
)
La ventana YuNet stream muestra recuadros verdes y puntos de referencia amarillos sobre los rostros detectados. Los fotogramas vacíos permanecen sin anotaciones. Activa esa ventana y pulsa q para detener el programa, o pulsa Ctrl+C en la terminal. Una lectura del archivo que devuelve false finaliza el bucle después de al menos un fotograma; OpenCV usa esa misma señal para el fin del archivo y los fallos de decodificación, por lo que esto no verifica la integridad de la grabación. Una captura no disponible, un fotograma vacío o un error del modelo provoca una salida con error y ejecuta la limpieza.
Para una cámara web, usa --camera 0 en lugar de
--video './input.mkv' en el mismo comando. Esa modalidad necesita una cámara, permiso del
sistema operativo y una pantalla de escritorio. El ejemplo con un archivo como fuente se probó
con una pantalla con interfaz gráfica; la captura desde una cámara web física sigue sin
verificarse. La falta de una pantalla puede hacer que el backend de la interfaz gráfica aborte
el proceso, así que comprueba ese requisito antes de iniciarlo.
Uso avanzado: manejo de varios rostros y ajustes de parámetros
La función auxiliar de dibujo ya recorre todas las detecciones. Cada fila contiene
(x, y, width, height), cinco puntos de referencia (x, y) y una
puntuación en el índice 14, como se documenta en el
tutorial de detección facial de OpenCV.
Para probar un umbral de puntuación más bajo, añade --score 0.7 al comando de
video. Es posible que se conserven más candidatos, incluidos falsos positivos.
--nms controla la supresión de solapamientos; su valor predeterminado es
0.3. El valor fijo de 5.000 para top_k limita
los candidatos antes de la supresión, en lugar de prometer 5.000 rostros. Compara las detecciones
en grabaciones representativas antes de cambiar estos ajustes.
Optimiza el rendimiento
Añade --scale 0.5 --every 2 al comando para realizar la inferencia sobre una imagen más
pequeña en el primer fotograma y, a partir de ahí, en uno de cada dos fotogramas. La vista previa
sigue mostrando todos los fotogramas decodificados. Los fotogramas omitidos no tienen anotaciones;
no reutilizan las detecciones del fotograma anterior. Omitir la inferencia no omite la
decodificación ni crea un seguimiento, y puede hacer que una aparición breve pase inadvertida.
El código vuelve a asignar los recuadros y los puntos de referencia a sus coordenadas originales usando el ancho y el alto reales de la imagen redimensionada. Usar proporciones separadas es importante cuando las dimensiones impares se redondean de forma diferente. Reducir la resolución también puede dificultar la detección de rostros pequeños. Mantén el único detector secuencial hasta que las mediciones justifiquen otro diseño; compartir su tamaño de entrada mutable entre llamadas concurrentes requeriría una política de sincronización independiente.
Caso de uso práctico: vigilancia de seguridad
Una vista previa de rostros puede ayudarte a inspeccionar grabaciones antes de crear una aplicación de vigilancia. Este programa no identifica visitantes, graba video, guarda detecciones ni envía alertas. El número de rostros en un fotograma procesado no equivale al número de personas únicas, y un resultado vacío u omitido no demuestra que nadie haya pasado por la escena.
Consideraciones de rendimiento
La terminal informa el tiempo medio dedicado a detect() por separado de la
tasa de procesamiento del bucle desde la captura hasta la visualización. El tiempo del bucle
incluye la captura, el redimensionamiento, la inferencia cuando se selecciona, el dibujo y la
gestión de eventos de la interfaz gráfica; excluye la inicialización del modelo y la limpieza
final. Un archivo como fuente se procesa sin regular el ritmo, por lo que su tasa de
procesamiento no es la frecuencia de fotogramas de la grabación ni la latencia de una cámara en
vivo.
Una ejecución con un archivo de video comprueba la captura, la inferencia y la visualización. No determina el rendimiento de una cámara web ni la precisión de la detección. Mide el rendimiento en el hardware y con la resolución que vayas a usar; luego inspecciona situaciones de iluminación difícil, oclusión, rostros pequeños y transiciones de rostros a fotogramas vacíos. Una medición de inferencia rápida por sí sola no demuestra que la aplicación completa pueda mantener el ritmo de un video en tiempo real.
Conclusión
Antes de usar una nueva fuente, inspecciona los recuadros y los puntos de referencia a tamaño completo; luego repite la prueba con las opciones de escalado u omisión que planees usar. Conserva la ejecución con un archivo como comprobación repetible cuando cambies el modelo o la versión de OpenCV. La guía de video de OpenCV explica las API de captura y visualización usadas aquí.
