Real-time face detection with OpenCV's YuNet
Use OpenCV’s YuNet detector to draw face boxes and five facial landmarks on a video preview. The Python example below processes a local video file or an optional webcam, keeps detections attached to their source frame, and releases the capture when you stop it.
Why real-time face detection?
A streaming preview lets you inspect how detections change as faces move, disappear, or become smaller. YuNet finds face locations; it does not recognize people or track a person across frames. Whether the loop keeps up with a live feed depends on your hardware and input resolution.
Setting up your environment
This Bash recipe was tested on Linux with Python 3.11.2, GUI opencv-python 4.13.0.92, and NumPy
2.2.6. These are tested versions, not intrinsic minimums. You need an installed python3 with venv
and pip, and a working desktop display. Other operating systems are outside this recipe’s
tested scope.
The pins deliberately keep the March 2023 model with OpenCV 4.x. OpenCV 5 is available, but its ONNX Runtime engine needs the newer dynamic-shape YuNet export for variable image sizes; see the YuNet model repository. Do not mix that upgrade into this example without testing the model and runtime together.
Paste this block from the directory where you want to create yunet-stream. It runs in a subshell,
so success and failure leave your shell in its original directory. It refuses an existing project.
(
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
)
If setup fails, inspect the partial directory and choose a fresh project name before repeating,
changing both mkdir and cd. The block does not remove existing files. The fresh virtual
environment keeps other OpenCV installations out of this recipe: the
OpenCV Python package guide requires choosing
one distribution, and headless packages cannot provide this preview window.
The model download has a 60-second socket timeout; a timeout or checksum failure stops setup
before publishing the model file. Check your connection before retrying in a fresh directory.
Building a basic streaming face detection pipeline
Save this complete program as yunet-stream/stream.py. It uses one detector sequentially and limits
OpenCV processing to one thread. Video decoding and the GUI can have their own threads; there is no
application worker queue.
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)
Place a local video that OpenCV can decode at yunet-stream/input.mkv, then paste this from the
parent directory. A filename containing spaces remains one quoted argument.
(
cd yunet-stream &&
.venv/bin/python stream.py --video './input.mkv'
)
The YuNet stream window shows green boxes and yellow landmarks on detected faces. Blank frames remain unannotated. Focus that window and press q to stop, or press Ctrl+C in the terminal. A file read returning false ends the loop after at least one frame; OpenCV uses that same signal for EOF and decoding failure, so this is not an integrity check for the recording. An unavailable capture, empty frame, or model error exits unsuccessfully and runs the cleanup.
For a webcam, use --camera 0 instead of --video './input.mkv' in the same command. That path needs
a camera, OS permission, and a desktop display. The file-feed example was exercised with a GUI
display; physical webcam capture remains unverified. A missing display can make the GUI backend
abort the process, so check that prerequisite before launching.
Advanced usage: handling multiple faces and parameter adjustments
The drawing helper already loops over every detection. Each row holds (x, y, width, height), five
(x, y) landmarks, and a score at index 14, as documented in the
OpenCV face detection tutorial.
To try a lower score threshold, add --score 0.7 to the video command. More candidates may survive,
including false positives. --nms controls overlap suppression; the default is 0.3. The fixed
top_k of 5,000 limits candidates before suppression, rather than promising 5,000 faces. Compare
detections on representative footage before changing these settings.
Optimizing performance
Add --scale 0.5 --every 2 to the command to infer on a smaller image on the first frame and every
second frame thereafter. The preview still displays every decoded frame. Skipped frames have no
annotations; they do not reuse the preceding frame’s detections. Skipping inference does not skip
decoding or create tracking, and it can miss a brief appearance.
The code maps boxes and landmarks back using the actual resized width and height. Separate ratios matter when odd dimensions round differently. Downsampling can also make small faces harder to detect. Keep the single sequential detector until measurements justify a different design; sharing its mutable input size across concurrent calls would require a separate synchronization policy.
Practical use case: security monitoring
A face preview can help you inspect footage before building a monitoring application. This program does not identify visitors, record video, persist detections, or send alerts. A face count on a processed frame is not a count of unique people, and an empty or skipped result does not establish that nobody passed through the scene.
Performance considerations
The terminal reports mean time spent in detect() separately from capture-to-display loop
throughput. Loop timing includes capture, resizing, inference when selected, drawing, and GUI
event handling; it excludes model initialization and final cleanup. A file feed runs unpaced, so
its throughput is not the recording’s frame rate or a live camera’s latency.
A video-file run checks capture, inference, and display. It does not establish webcam performance or detection accuracy. Measure on your target hardware and resolution, then inspect difficult lighting, occlusion, small faces, and face-to-blank transitions. A fast inference measurement alone does not show that the complete application can sustain real-time video.
Conclusion
Before using a new feed, inspect the boxes and landmarks at full size, then repeat with any scaling or skipping options you plan to use. Keep the file run as a repeatable check when changing the model or OpenCV version. The OpenCV video guide explains the capture and display APIs used here.
