Annoter images et vidéos avec YuNet et l’automatisation en CLI
YuNet est un détecteur de visages compact disponible dans OpenCV. Une petite CLI peut l’utiliser pour annoter des images locales et de courtes vidéos à cadence constante, en traçant des rectangles verts autour des visages détectés. Les exemples ci-dessous préservent les dimensions des images décodées et produisent une sortie distincte pour chaque entrée du lot. La détection repère des régions ressemblant à des visages ; elle n’identifie pas les personnes et ne fournit pas de décision d’authentification.
Comprendre YuNet
YuNet offre une solution légère, conçue pour le CPU, pour détecter les visages et les points de repère faciaux. La précision et la vitesse dépendent de la taille de l’image, de celle des visages, de leur pose, de l’éclairage, du seuil et du matériel. Consultez la documentation du modèle dans OpenCV Zoo pour connaître son évaluation publiée. Ce tutoriel traite des fichiers locaux ; il ne démontre pas les performances en temps réel avec une caméra.
Configurer rapidement YuNet
Utilisez Bash sous Linux, Python avec venv et pip, cURL, ainsi qu’un système de fichiers
prenant en charge les liens physiques. Ce tutoriel a été testé sous Linux x86-64 avec Python 3.14.7,
le paquet wheel opencv-python-headless 5.0.0.93 (OpenCV 5.0.0) et NumPy 2.4.6.
La version corrective de Python et la version du paquet wheel sont des versions fixes testées ;
le traitement vidéo utilise le statut d’écriture booléen d’OpenCV 5. Utilisez une
version de Python prise en charge avec un paquet wheel compatible provenant de la
page des versions du paquet.
Les autres combinaisons d’environnement d’exécution et de plateforme nécessitent leur propre validation.
Commencez dans un répertoire de travail vide. Cette CLI n’ouvre aucune fenêtre graphique ; installez
donc le paquet wheel sans interface graphique. Les paquets wheel d’OpenCV partagent l’espace de noms
cv2 : installez une seule variante par environnement.
(
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
)
Les parenthèses maintiennent la gestion des échecs dans un sous-shell. Si la création de
l’environnement échoue, l’exécution s’arrête avant l’installation ; si l’installation échoue, elle
peut laisser un venv partiel. Poursuivez dans un nouveau répertoire vide
au lieu de réutiliser cet environnement. Chaque commande ci-dessous indique explicitement son
interpréteur ; vous n’avez donc pas besoin d’activer l’environnement.
Téléchargez les octets du modèle testé depuis la révision fixe d’OpenCV Zoo et vérifiez leur somme de contrôle avant de rendre le fichier disponible sous le nom utilisé par les 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
)
Le modèle fait 232 589 octets. Si la requête ou la vérification de la somme de contrôle échoue, le fichier téléchargé temporaire est supprimé et le fichier portant le nom final reste absent ; réexécutez le même bloc après avoir corrigé la cause de l’échec. Un modèle existant est préservé. Conservez-le à côté des trois scripts ci-dessous.
Automatiser la détection sur une seule image
Enregistrez cette implémentation partagée sous le nom face_tools.py.
L’API FaceDetectorYN d’OpenCV exige que la taille d’entrée corresponde
à celle de l’image reçue. La fonction auxiliaire redimensionne l’image utilisée pour l’inférence,
puis remet les rectangles détectés à l’échelle de l’image originale pour les tracer.
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()
Enregistrez ce script appelant sous le nom detect_faces.py dans le même répertoire :
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')
Exécutez venv/bin/python detect_faces.py image photo.jpg annotated.jpg avec votre propre fichier JPEG ou PNG.
Ouvrez le résultat et vérifiez que les rectangles verts entourent les visages attendus. Une image
valide sans visage détecté produit tout de même une sortie et signale zéro détection. Utilisez
.jpg ou .png pour la sortie et créez d’abord
son répertoire parent ; les destinations existantes sont refusées.
OpenCV décode ces images en pixels couleur sur 8 bits. La transparence, les profondeurs de couleur supérieures, les profils colorimétriques et les métadonnées d’origine ne sont pas préservés. Les dimensions sont celles de l’image décodée, après application automatique de l’orientation EXIF par OpenCV. La sortie JPEG entraîne des pertes ; utilisez PNG si vous avez besoin de conserver les pixels inchangés en dehors des rectangles. Lors des tests, un fichier JPEG privé de ses derniers octets a tout de même été décodé avec succès, les pixels manquants ayant été remplis. Inspectez l’image entière, y compris ses bords, et utilisez des fichiers d’entrée intacts et fiables ; cette CLI ne valide pas l’intégrité des entrées.
Traiter des dossiers par lots
Réutilisez un seul détecteur de manière séquentielle pour les fichiers JPEG et PNG situés directement
dans un dossier. Enregistrez ce script sous le nom 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')
Exécutez venv/bin/python batch_detect.py images new-results. Le répertoire des résultats doit être nouveau. La
comparaison des extensions ne tient pas compte de la casse, les fichiers sont triés par nom, et les
sous-dossiers ainsi que les liens symboliques sont ignorés. Les noms complets des fichiers source sont
conservés avant l’ajout de .jpg ; ainsi, photo.jpg
et photo.png produisent photo.jpg.jpg et
photo.png.jpg. Les fichiers dont le traitement échoue sont nommés sur stderr et
entraînent un code de retour non nul pour le lot ; les résultats réussis restent disponibles.
Réessayez dans un autre nouveau répertoire de résultats.
Placez entre guillemets les chemins contenant des espaces. Pour un nom d’entrée ou de destination
commençant par un trait d’union, fournissez un chemin tel que ./-photo.png afin
que argparse n’interprète pas le nom comme une option.
Traiter les flux vidéo
Exécutez venv/bin/python detect_faces.py video input.mp4 annotated.mp4 pour une courte vidéo locale à cadence constante dont
les dimensions décodées sont paires. La sortie contient uniquement la vidéo annotée :
l’audio n’est pas copié. Le total des détections compte les observations sur l’ensemble des images,
et non les personnes distinctes. Le respect du minutage à cadence variable et la reconnexion à une
caméra en direct nécessitent un autre processus de traitement.
Un échec de lecture d’image dans OpenCV peut indiquer la fin du fichier ou un problème de décodeur. Un extrait de test tronqué au milieu d’un paquet proche de la fin a produit un résultat plus court, mais lisible, et un message de réussite. Le module d’écriture de la version fixe renvoie un statut pour chaque image, que cette CLI vérifie, mais, avec certains moteurs, le statut de réussite reste indicatif et ne garantit pas l’écriture complète de l’image. Comparez le nombre d’images décodées et la cadence du résultat avec ceux de la source, vérifiez que sa durée correspond au nombre d’images divisé par la cadence, et lisez le début et la fin. Son message de réussite ne prouve pas à lui seul l’intégrité complète du média. Les métadonnées de rotation peuvent modifier les dimensions codées ; comparez les images décodées affichées, que la sortie encode avec leur orientation appliquée.
Optimiser la vitesse pour le temps réel
Réduisez max_side dans annotate pour redimensionner
effectivement les images utilisées pour l’inférence, puis mesurez le compromis sur les visages petits
et éloignés. Conservez un détecteur par processus de traitement ; ne le partagez pas entre des appels
simultanés. Un paquet wheel pour CPU ne fournit pas automatiquement la prise en charge de CUDA.
L’accélération GPU nécessite une compilation compatible d’OpenCV, du matériel compatible et une
configuration du moteur validée séparément.
Pour les médias non fiables, utilisez des processus de traitement isolés et imposez des limites de durée totale, de mémoire et d’espace disque pour l’ensemble de la tâche. Les vérifications du nombre d’octets et de pixels des images ne limitent pas toutes les allocations du décodeur vidéo ni la durée totale du traitement.
Comparez un autre détecteur sur les mêmes visages représentatifs et le même matériel si YuNet n’atteint pas votre objectif de précision ou de latence. Incluez le décodage, le redimensionnement, la détection et l’encodage dans la durée mesurée ; le temps d’inférence seul ne permet pas de savoir si la chaîne complète suit le rythme d’une source en direct.
Pour des processus de vision gérés, consultez le service d’intelligence artificielle de Transloadit.
