Anota imágenes y videos con YuNet y automatización por CLI
YuNet es un detector de rostros compacto disponible a través de OpenCV. Una pequeña CLI puede usarlo para anotar imágenes locales y videos cortos con tasa de fotogramas constante, dibujando recuadros verdes alrededor de los rostros detectados. Los ejemplos siguientes conservan las dimensiones de las imágenes decodificadas y generan una salida propia para cada entrada del lote. La detección encuentra regiones similares a rostros; no identifica personas ni determina si se debe autorizar el acceso.
Comprende YuNet
YuNet ofrece una opción ligera orientada a CPU para detectar rostros y puntos de referencia faciales. La precisión y la velocidad dependen del tamaño de la imagen, el tamaño del rostro, la pose, la iluminación, el umbral y el hardware. Consulta la documentación del modelo de OpenCV Zoo para ver la evaluación publicada del modelo. Este tutorial procesa archivos locales; no demuestra el rendimiento de una cámara en tiempo real.
Configura YuNet rápidamente
Usa Bash en Linux, Python con venv y pip, cURL y un sistema de archivos que
admita enlaces duros. Este tutorial se probó en Linux x86-64 con Python 3.14.7, el wheel
opencv-python-headless 5.0.0.93 (OpenCV 5.0.0) y NumPy 2.4.6. La versión de parche de Python y
la compilación del wheel son versiones fijadas y probadas; el procesamiento de video usa el estado
booleano de escritura de OpenCV 5. Usa una
versión de Python con soporte con un wheel compatible de la
página de versiones del paquete.
Otras combinaciones de entorno de ejecución y plataforma necesitan su propia validación.
Comienza en un directorio de trabajo vacío. Esta CLI no abre ventanas gráficas, así que instala el
wheel sin interfaz gráfica. Los wheels de OpenCV comparten el espacio de nombres
cv2: instala una variante por entorno.
(
set -e
command -v python3 >/dev/null || { printf 'Missing prerequisite: python3\n' >&2; exit 1; }
if [ -e venv ] || [ -L venv ]; then
printf 'Choose a fresh directory; venv already exists\n' >&2
exit 1
fi
python3 -m venv venv
venv/bin/python -m pip install opencv-python-headless==5.0.0.93 numpy==2.4.6
)
Los paréntesis mantienen el manejo de errores dentro de una subshell. Si falla la creación del
entorno, el proceso se detiene antes de la instalación; si falla la instalación, puede quedar un
entorno venv incompleto. Continúa en un directorio nuevo y vacío en lugar de
reutilizar ese entorno. Cada comando siguiente especifica su intérprete de forma explícita, por lo
que no necesitas activar el entorno.
Descarga los bytes del modelo probado desde la revisión fijada de OpenCV Zoo y verifica su suma de comprobación antes de publicarlo con el nombre de archivo que usan los scripts:
(
set -e
for tool in curl mktemp ln rm; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done
test -x venv/bin/python
if [ -e face_detection_yunet.onnx ] || [ -L face_detection_yunet.onnx ]; then
printf 'Model already exists; verify it or choose a fresh directory\n' >&2
exit 1
fi
model_tmp=$(mktemp './.yunet-model.XXXXXX')
trap 'rm -f "$model_tmp"' EXIT
curl -fsSLo "$model_tmp" \
https://media.githubusercontent.com/media/opencv/opencv_zoo/47534e27c9851bb1128ccc0102f1145e27f23f98/models/face_detection_yunet/face_detection_yunet_2023mar.onnx
venv/bin/python - "$model_tmp" <<'PY'
import hashlib
from pathlib import Path
import sys
expected = '8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4'
if hashlib.sha256(Path(sys.argv[1]).read_bytes()).hexdigest() != expected:
sys.exit('Unexpected model checksum')
PY
ln "$model_tmp" face_detection_yunet.onnx
)
El modelo ocupa 232.589 bytes. Si falla la solicitud o la verificación de la suma de comprobación, se elimina la descarga temporal y el archivo con el nombre definitivo queda ausente; vuelve a ejecutar el mismo bloque después de resolver el error. Si ya existe un modelo, se conserva. Mantenlo junto a los tres scripts siguientes.
Automatiza la detección en una sola imagen
Guarda esta implementación compartida como face_tools.py. La
API FaceDetectorYN de OpenCV espera que su tamaño de entrada coincida
con el fotograma que recibe. La función auxiliar redimensiona el fotograma de inferencia y luego
ajusta la escala de los recuadros detectados a la imagen original para dibujarlos.
from contextlib import contextmanager
import math
import os
from pathlib import Path
from tempfile import TemporaryDirectory
# Set before importing OpenCV so image decoding has an explicit pixel ceiling.
os.environ.setdefault('OPENCV_IO_MAX_IMAGE_PIXELS', '20000000')
import cv2
MODEL_PATH = Path(__file__).with_name('face_detection_yunet.onnx')
def create_detector():
return cv2.FaceDetectorYN.create(str(MODEL_PATH), '', (320, 320), 0.9, 0.3, 5000)
def annotate(detector, image, max_side=960):
height, width = image.shape[:2]
if width * height > 20_000_000:
raise ValueError('Frame exceeds the pixel limit')
scale = min(1.0, max_side / max(width, height))
small = cv2.resize(image, (max(1, round(width * scale)), max(1, round(height * scale))))
detector.setInputSize((small.shape[1], small.shape[0]))
_, faces = detector.detect(small)
count = 0 if faces is None else len(faces)
if faces is not None:
scale_x = width / small.shape[1]
scale_y = height / small.shape[0]
for face in faces:
x, y, w, h = face[:4]
left, top = max(0, round(x * scale_x)), max(0, round(y * scale_y))
right = min(width - 1, round((x + w) * scale_x))
bottom = min(height - 1, round((y + h) * scale_y))
cv2.rectangle(image, (left, top), (right, bottom), (0, 255, 0), 2)
return image, count
@contextmanager
def new_output(path):
output = Path(path).absolute()
if output.exists() or output.is_symlink():
raise ValueError('Output must not exist')
with TemporaryDirectory(prefix='.face-output-', dir=output.parent) as temporary:
candidate = Path(temporary) / ('result' + output.suffix)
yield candidate
if not candidate.is_file() or candidate.stat().st_size == 0:
raise ValueError('No nonempty output produced')
os.link(candidate, output)
def detect_image(detector, source, output):
source = Path(source)
if source.suffix.lower() not in {'.jpg', '.jpeg', '.png'}:
raise ValueError('Use a JPEG or PNG image')
if not source.is_file() or source.stat().st_size > 50 * 1024 * 1024:
raise ValueError('Use a regular image file no larger than 50 MiB')
image = cv2.imread(str(source))
if image is None:
raise ValueError('Cannot decode image')
annotated, count = annotate(detector, image)
with new_output(output) as candidate:
if not cv2.imwrite(str(candidate), annotated):
raise ValueError('Cannot encode output image')
return count
def detect_video(detector, source, output):
if not Path(source).is_file():
raise ValueError('Use a local video file')
capture = cv2.VideoCapture(str(Path(source).absolute()))
writer = None
frames = 0
detections = 0
try:
if not capture.isOpened():
raise ValueError('Cannot open video')
fps = capture.get(cv2.CAP_PROP_FPS)
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
if not math.isfinite(fps) or fps <= 0 or width <= 0 or height <= 0:
raise ValueError('Invalid video timing or dimensions')
if width * height > 20_000_000 or width % 2 or height % 2:
raise ValueError('Use bounded, even video dimensions')
with new_output(output) as candidate:
writer = cv2.VideoWriter(str(candidate), cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height))
if not writer.isOpened():
raise ValueError('Cannot open video encoder')
while True:
ok, frame = capture.read()
if not ok:
break
if frame.shape[:2] != (height, width):
raise ValueError('Frame dimensions changed')
annotated, count = annotate(detector, frame)
if not writer.write(annotated):
raise ValueError('Cannot encode video frame')
frames += 1
detections += count
writer.release()
writer = None
if frames == 0:
raise ValueError('No frames decoded')
return frames, detections
finally:
capture.release()
if writer is not None:
writer.release()
Guarda este script que llama a la implementación como detect_faces.py en el mismo
directorio:
import argparse
import sys
from face_tools import create_detector, detect_image, detect_video
import cv2
parser = argparse.ArgumentParser()
parser.add_argument('mode', choices=['image', 'video'])
parser.add_argument('input')
parser.add_argument('output')
args = parser.parse_args()
try:
detector = create_detector()
if args.mode == 'image':
print(f'Detected {detect_image(detector, args.input, args.output)} faces')
else:
frames, detections = detect_video(detector, args.input, args.output)
print(f'Processed {frames} frames with {detections} face detections')
except (OSError, ValueError, cv2.error):
sys.exit(f'{args.input}: Face detection failed; check input, model, codec, and destination')
Ejecuta venv/bin/python detect_faces.py image photo.jpg annotated.jpg con tu propio archivo JPEG o PNG.
Abre el resultado y comprueba que los recuadros verdes rodeen los rostros que esperas. Una imagen
válida sin rostros detectados también genera una salida e informa de cero detecciones. Usa
.jpg o .png para la salida y crea primero su
directorio padre; se rechazan los destinos existentes.
OpenCV decodifica estas imágenes como píxeles de color de 8 bits. No se conservan la transparencia, una mayor profundidad de bits, los perfiles de color ni los metadatos de origen. Las dimensiones corresponden a la imagen decodificada, incluida la orientación EXIF automática de OpenCV. La salida JPEG tiene pérdida; usa PNG cuando necesites conservar sin cambios los píxeles fuera de los recuadros. Durante las pruebas, un JPEG al que se le habían eliminado los últimos bytes se decodificó correctamente y los píxeles faltantes se rellenaron. Inspecciona toda la imagen, incluidos sus bordes, y usa entradas íntegras y confiables; esta CLI no valida la integridad de las entradas.
Procesa carpetas por lotes
Reutiliza un detector de forma secuencial para los archivos JPEG y PNG del nivel superior de una
carpeta. Guarda este script como batch_detect.py:
import argparse
from pathlib import Path
import sys
from face_tools import create_detector, detect_image
import cv2
parser = argparse.ArgumentParser()
parser.add_argument('input_directory')
parser.add_argument('output_directory')
args = parser.parse_args()
try:
source = Path(args.input_directory)
images = sorted(path for path in source.iterdir() if path.is_file() and not path.is_symlink()
and path.suffix.lower() in {'.jpg', '.jpeg', '.png'})
if not images:
sys.exit('No supported images found')
detector = create_detector()
output = Path(args.output_directory)
output.mkdir(mode=0o700)
failures = 0
for image in images:
try:
count = detect_image(detector, image, output / (image.name + '.jpg'))
print(f'{image.name}: {count} faces')
except (OSError, ValueError, cv2.error):
failures += 1
print(f'Failed: {image.name}', file=sys.stderr)
sys.exit(1 if failures else 0)
except (OSError, ValueError, cv2.error):
sys.exit('Cannot prepare batch; check model and directories')
Ejecuta venv/bin/python batch_detect.py images new-results. El directorio de resultados debe ser nuevo. La coincidencia
de sufijos no distingue entre mayúsculas y minúsculas, los archivos se ordenan por nombre y se
omiten las subcarpetas y las entradas de enlaces simbólicos. Se conservan los nombres completos de
los archivos de origen antes de añadir .jpg, por lo que
photo.jpg y photo.png generan
photo.jpg.jpg y photo.png.jpg. Los archivos cuyo procesamiento falla
se identifican por nombre en stderr y hacen que el estado del lote sea distinto de cero; los
resultados correctos siguen disponibles. Vuelve a intentarlo en otro directorio de resultados nuevo.
Pon entre comillas las rutas que contengan espacios. Para un nombre de entrada o destino que empiece
con un guion, pasa una ruta como ./-photo.png para que
argparse no interprete el nombre como una opción.
Maneja flujos de video
Ejecuta venv/bin/python detect_faces.py video input.mp4 annotated.mp4 para un video local corto con tasa de fotogramas constante y
dimensiones decodificadas pares. La salida contiene únicamente el video anotado:
el audio no se copia. El total de detecciones cuenta las observaciones a lo largo de los
fotogramas, no las personas distintas. La temporización con tasa de fotogramas variable y la
reconexión de cámaras en directo requieren un flujo de trabajo diferente.
Un fallo de lectura de fotograma en OpenCV puede indicar el final del archivo o un problema del decodificador. Un clip de prueba truncado dentro de un paquete cercano al final produjo un resultado reproducible más corto y un mensaje de éxito. El escritor de la versión fijada devuelve un estado por fotograma, que esta CLI comprueba, pero algunos backends solo informan de un éxito sin garantía. Compara el número de fotogramas decodificados y la tasa de fotogramas del resultado con los del origen, comprueba que su duración sea el número de fotogramas dividido por la tasa de fotogramas y reproduce el principio y el final. Su mensaje de éxito por sí solo no demuestra la integridad completa del contenido multimedia. Los metadatos de rotación pueden cambiar las dimensiones codificadas; compara los fotogramas decodificados que se muestran, ya que la salida los codifica con esa orientación.
Ajusta la velocidad para el tiempo real
Reduce max_side en annotate para redimensionar realmente
los fotogramas de inferencia y luego evalúa el equilibrio entre velocidad y precisión en rostros
pequeños y distantes. Mantén un detector por proceso de trabajo; no lo compartas entre llamadas
simultáneas. Un wheel para CPU no proporciona soporte para CUDA de forma automática. La aceleración
por GPU requiere una compilación de OpenCV compatible, hardware y una configuración del backend
validada por separado.
Para contenido multimedia no confiable, usa procesos de trabajo aislados y límites de tiempo, memoria y disco para el procesamiento completo. Las comprobaciones de bytes y píxeles de las imágenes no limitan todas las asignaciones de memoria del decodificador de video ni el tiempo total de procesamiento.
Compara otro detector con los mismos rostros representativos y el mismo hardware si YuNet no cumple tu objetivo de precisión o latencia. Incluye la decodificación, el redimensionamiento, la detección y el encoding en el tiempo transcurrido; el tiempo de inferencia por sí solo no indica si el pipeline completo puede mantener el ritmo de una fuente en directo.
Para flujos de trabajo de visión gestionados, consulta el servicio de inteligencia artificial de Transloadit.
