Detecção facial em tempo real com o YuNet do OpenCV
Use o detector YuNet do OpenCV para desenhar caixas ao redor dos rostos e cinco pontos de referência faciais em uma prévia de vídeo. O exemplo em Python abaixo processa um arquivo de vídeo local ou, opcionalmente, uma webcam, mantém as detecções vinculadas ao quadro de origem e libera a captura quando você o interrompe.
Por que detectar rostos em tempo real?
Uma prévia em fluxo contínuo permite examinar como as detecções mudam conforme os rostos se movem, desaparecem ou ficam menores. O YuNet localiza rostos; ele não reconhece pessoas nem acompanha uma pessoa ao longo dos quadros. A capacidade do loop de acompanhar uma transmissão em tempo real depende do seu hardware e da resolução de entrada.
Configurar seu ambiente
Esta receita em Bash foi testada no Linux com Python 3.11.2, a versão com GUI de
opencv-python 4.13.0.92 e NumPy 2.2.6. Essas são versões testadas, não requisitos
mínimos intrínsecos. Você precisa de python3 instalado com
venv e pip, além de uma interface gráfica funcional.
Outros sistemas operacionais estão fora do escopo testado desta receita.
As versões fixadas mantêm intencionalmente o modelo de março de 2023 com o OpenCV 4.x. O OpenCV 5 está disponível, mas seu mecanismo ONNX Runtime precisa da exportação mais recente do YuNet com dimensões dinâmicas para imagens de tamanhos variáveis; consulte o repositório do modelo YuNet. Não incorpore essa atualização ao exemplo sem testar o modelo e o ambiente de execução juntos.
Cole este bloco a partir do diretório onde você quer criar yunet-stream. Ele é
executado em um subshell, então seu shell permanece no diretório original tanto em caso de sucesso
quanto de falha. O bloco se recusa a usar um projeto 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
)
Se a configuração falhar, examine o diretório parcial e escolha um novo nome de projeto antes de
repetir, alterando tanto mkdir quanto cd. O bloco
não remove arquivos existentes. O novo ambiente virtual mantém outras instalações do OpenCV fora
desta receita: o guia do pacote Python do OpenCV exige que você escolha
uma única distribuição, e os pacotes sem interface gráfica não podem fornecer esta janela de prévia.
O download do modelo tem um tempo limite de socket de 60 segundos; um tempo limite excedido ou uma
falha na soma de verificação interrompe a configuração antes de publicar o arquivo do modelo.
Verifique sua conexão antes de tentar novamente em um novo diretório.
Construir um pipeline básico de detecção facial em fluxo contínuo
Salve este programa completo como yunet-stream/stream.py. Ele usa um único detector de forma
sequencial e limita o processamento do OpenCV a uma thread. A decodificação de vídeo e a GUI podem
ter suas próprias threads; não há uma fila de unidades de execução da aplicação.
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)
Coloque um vídeo local que o OpenCV consiga decodificar em yunet-stream/input.mkv e cole o
bloco abaixo a partir do diretório pai. Um nome de arquivo com espaços continua sendo um único
argumento entre aspas.
(
cd yunet-stream &&
.venv/bin/python stream.py --video './input.mkv'
)
A janela YuNet stream mostra caixas verdes e pontos de referência amarelos nos rostos detectados. Quadros em branco permanecem sem anotações. Coloque o foco nessa janela e pressione q para parar, ou pressione Ctrl+C no terminal. Uma leitura do arquivo que retorna false encerra o loop após pelo menos um quadro; o OpenCV usa esse mesmo sinal para EOF e falha de decodificação, então isso não é uma verificação de integridade da gravação. Uma captura indisponível, um quadro vazio ou um erro no modelo faz o processo terminar sem sucesso e executar a liberação dos recursos.
Para uma webcam, use --camera 0 em vez de --video './input.mkv' no mesmo
comando. Essa opção exige uma câmera, permissão do sistema operacional e uma interface gráfica.
O exemplo com arquivo foi executado com uma interface gráfica; a captura por webcam física ainda
não foi verificada. A ausência de uma interface gráfica pode fazer o backend da GUI abortar o
processo, então verifique esse pré-requisito antes de iniciar.
Uso avançado: lidar com múltiplos rostos e ajustar parâmetros
A função auxiliar de desenho já percorre todas as detecções. Cada linha contém
(x, y, width, height), cinco pontos de referência (x, y) e uma
pontuação no índice 14, conforme documentado no
tutorial de detecção facial do OpenCV.
Para experimentar um limiar de pontuação menor, adicione --score 0.7 ao comando de
vídeo. Mais candidatos podem permanecer, incluindo falsos positivos. --nms
controla a supressão de sobreposições; o padrão é 0.3. O valor fixo de 5.000
para top_k limita os candidatos antes da supressão, em vez de prometer 5.000
rostos. Compare as detecções em gravações representativas antes de alterar essas configurações.
Otimizar o desempenho
Adicione --scale 0.5 --every 2 ao comando para executar a inferência em uma imagem menor no
primeiro quadro e, depois, a cada dois quadros. A prévia continua exibindo todos os quadros
decodificados. Os quadros pulados não têm anotações; eles não reutilizam as detecções do quadro
anterior. Pular a inferência não pula a decodificação nem cria rastreamento, e pode deixar passar uma
aparição breve.
O código mapeia as caixas e os pontos de referência de volta usando a largura e a altura reais após o redimensionamento. Proporções separadas são importantes quando dimensões ímpares são arredondadas de formas diferentes. A redução da resolução também pode dificultar a detecção de rostos pequenos. Mantenha o detector único e sequencial até que as medições justifiquem outro projeto; compartilhar seu tamanho de entrada mutável entre chamadas simultâneas exigiria uma política de sincronização separada.
Caso de uso prático: monitoramento de segurança
Uma prévia de rostos pode ajudar você a examinar gravações antes de criar uma aplicação de monitoramento. Este programa não identifica visitantes, grava vídeos, persiste detecções nem envia alertas. Uma contagem de rostos em um quadro processado não é uma contagem de pessoas distintas, e um resultado vazio ou pulado não comprova que ninguém passou pela cena.
Considerações sobre desempenho
O terminal informa o tempo médio gasto em detect() separadamente da taxa de
processamento do loop de captura até a exibição. A medição do loop inclui captura, redimensionamento,
inferência quando selecionada, desenho e tratamento de eventos da GUI; ela exclui a inicialização do
modelo e a liberação final dos recursos. A leitura de um arquivo ocorre sem controle de cadência,
então sua taxa de processamento não corresponde à taxa de quadros da gravação nem à latência de uma
câmera em tempo real.
Uma execução com arquivo de vídeo verifica captura, inferência e exibição. Ela não comprova o desempenho da webcam nem a precisão da detecção. Faça medições no hardware e na resolução que você pretende usar e examine condições de iluminação difíceis, oclusões, rostos pequenos e transições de quadros com rostos para quadros em branco. Uma medição de inferência rápida, por si só, não demonstra que a aplicação completa consegue sustentar vídeo em tempo real.
Conclusão
Antes de usar uma nova fonte de vídeo, examine as caixas e os pontos de referência em tamanho original e repita com as opções de redimensionamento ou salto de quadros que você pretende usar. Mantenha a execução com arquivo como uma verificação repetível ao alterar o modelo ou a versão do OpenCV. O guia de vídeo do OpenCV explica as APIs de captura e exibição usadas aqui.
