Ultralytics YOLO27: Inscreve-te na lista de espera.
Get Started
Ultralytics YOLO em produção·Calibrar e observar·Aula 7/9
Aulaintermediário

Inferência em múltiplos fluxos

Executa várias câmeras num único modelo com inferência em lotes, filas limitadas e medição de frames descartados.

Um sistema que acompanha uma câmera pode ficar minutos atrasado com quatro, sem que nada falhe: os resultados simplesmente chegam cada vez mais tarde, até a memória esgotar. No CPU do portátil usado aqui, um fluxo de 30 FPS acompanhou o ritmo; quatro descartaram metade dos frames e oito descartaram mais de dois terços. Esta lição cria o pipeline que mede esse valor, porque é assim que dimensionas o hardware.

Resultado

Executa dois ou mais fluxos num único modelo, informa os frames processados e descartados por fluxo e encontra a quantidade de fluxos a partir da qual o teu hardware deixa de acompanhar o ritmo.

Via rápida
Se já sabes orientar-te, aqui está a versão resumida.
  1. Integrado: lista as fontes num ficheiro .streams e chama model.track("cams.streams", stream=True).

  2. stream_buffer=False (predefinição) mantém apenas o frame mais recente de cada fluxo; True processa todos os frames de ficheiros gravados.

  3. Personalizado: threads de descodificação, uma fila limitada e um loop consumidor que controla o modelo e os lotes.

  4. Conta os frames descartados por fluxo; a capacidade é a maior quantidade de fluxos sem descartes ao teu FPS.

Prático

Para onde vai o tempo#

NVIDIA DeepStream a executar Ultralytics YOLO em quatro fluxos de câmera a cerca de 12 FPS cada num Jetson, com registos PERF por fluxo junto a uma grelha de deteções 2×2

Cada frame passa por quatro etapas:

EtapaCusto típicoExecuta em
Descodificação (RTSP ou ficheiro → array)5–20 ms por frame 1080pCPU
Pré-processamento (letterbox, normalização)cerca de 1 msCPU
Inferênciaalguns ms (GPU) a dezenas de ms (CPU)Acelerador
Pós-processamento, rastreamento, destinosalguns ms, mais para bases de dados lentasCPU

Executa as etapas uma após a outra, câmera a câmera, numa única thread, e o acelerador fica ocioso enquanto os frames são descodificados. A solução é separar as etapas, como faz o pipeline DeepStream acima: quatro fluxos partilham um dispositivo, cada um indicando o seu próprio FPS (cerca de 12 aqui). A mesma estrutura em Python:

Integrado: um ficheiro .streams#

O Ultralytics agrupa várias fontes em lotes por ti. Coloca uma fonte por linha num ficheiro .streams e passa-o ao modo Predict ou Track. Cada fluxo recebe a sua própria thread de leitura e o seu próprio rastreador, e cada iteração executa uma passagem direta em lote por todos os fluxos:

from collections import Counter
from pathlib import Path

from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download

base = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
clips = [safe_download(base + name) for name in ("solutions_ci_demo.mp4", "ground-vehicles-airport.mp4")]
Path("cams.streams").write_text("\n".join(str(c) for c in clips))  # produção: um URL rtsp:// por linha

model = YOLO("yolo26n.pt")  # usa a tua exportação otimizada em produção
frames = Counter()
for r in model.track("cams.streams", stream=True, stream_buffer=True, tracker="bytetrack.yaml", verbose=False):
    frames[Path(r.path).name] += 1  # r.path indica de que fluxo veio este resultado
print(frames)
Counter({'solutions_ci_demo.mp4': 62, 'ground-vehicles-airport.mp4': 62})

Ambos os fluxos produziram 62 resultados porque a iteração termina quando acaba o fluxo mais curto (o clipe do aeroporto tem 150 frames). stream_buffer=True processou todos os frames, o que é adequado para ficheiros gravados; para câmeras ao vivo, mantém a predefinição False para que um modelo lento ignore frames desatualizados em vez de ficar para trás. Combina isto com uma exportação TensorRT ou OpenVINO para obter o melhor débito por fluxo.

Personalizado: produtor-consumidor com contadores de descartes#

Três diagramas de temporização: um processo com uma única thread espera por cada pedido de E/S, um processo com várias threads sobrepõe as esperas de E/S, mas apenas a thread que detém o GIL processa de cada vez; asyncio sobrepõe as esperas numa única thread

O diagrama mostra por que as threads funcionam neste pipeline. A descodificação consiste sobretudo em esperar (as barras cor-de-rosa de E/S), e OpenCV e PyTorch libertam o GIL do Python enquanto trabalham, por isso as threads de descodificação sobrepõem-se bem (painel b). O pós-processamento intenso em Python não se sobrepõe; transfere-o para processos separados.

Quando precisares dos teus próprios destinos, de lógica por câmera ou de métricas explícitas de descarte, constrói o pipeline por tua conta. As threads de descodificação nunca tocam no modelo; um único loop consumidor controla-o, o que também te mantém dentro das regras de inferência segura para threads:

import queue
import threading
import time

import cv2

from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download

base = "https://github.com/ultralytics/assets/releases/download/v0.0.0/"
SOURCES = [str(safe_download(base + n)) for n in ("solutions_ci_demo.mp4", "ground-vehicles-airport.mp4")]
frames = queue.Queue(maxsize=16)  # bounded: memory and latency stay flat under overload
dropped = [0] * len(SOURCES)

def decode(stream_id, source):
    """Producer: read frames at the camera's pace and never block on a full queue."""
    cap = cv2.VideoCapture(source)
    interval = 1 / (cap.get(cv2.CAP_PROP_FPS) or 30)
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        try:
            frames.put_nowait((stream_id, frame))
        except queue.Full:
            dropped[stream_id] += 1  # consumer is behind: drop rather than serve stale frames
        time.sleep(interval)  # pace files like a live camera; remove for RTSP sources
    cap.release()

model = YOLO("yolo26n.pt")  # one model, used only by the consumer loop below
model.predict("https://ultralytics.com/images/bus.jpg", verbose=False)  # warm up before cameras start

decoders = [threading.Thread(target=decode, args=(i, s), daemon=True) for i, s in enumerate(SOURCES)]
for t in decoders:
    t.start()

processed = [0] * len(SOURCES)
while any(t.is_alive() for t in decoders) or not frames.empty():
    batch = []
    while len(batch) < 8:  # gather up to 8 frames from any camera
        try:
            batch.append(frames.get(timeout=0.05))
        except queue.Empty:
            break
    if not batch:
        continue
    results = model([frame for _, frame in batch], verbose=False)  # one batched forward pass
    for (stream_id, _), r in zip(batch, results):
        processed[stream_id] += 1  # per-stream postprocessing and sinks go here

print(f"processed={processed} dropped={dropped}")
processed=[56, 131] dropped=[6, 19]

Os dois clipes têm 62 e 150 frames, e cada frame é processado ou descartado (56 + 6 = 62, 131 + 19 = 150), portanto os contadores contabilizam tudo. Foram descartados 25 dos 212 frames: o CPU deste portátil não consegue executar yolo26n em PyTorch com dois fluxos de 30 FPS. Repetindo os clipes para simular mais câmeras, numa única execução:

Fluxos (30 FPS cada)Frames descartados
10%
212%
451%
869%

Dimensiona a capacidade com base em medições#

Fluxos por dispositivo ≈ débito sustentado (frames por segundo que o modelo processa com o tamanho de lote escolhido) ÷ FPS necessário por fluxo. Mede o débito com a exportação otimizada das lições anteriores no hardware real, mantém uma margem de 20–30% para picos e gera um alerta quando os descartes forem superiores a zero. O custo de resoluções mais altas aumenta aproximadamente com o número de píxeis: 1280×1280 exige cerca de 4× mais capacidade de computação do que 640×640. Para grandes frotas, o Triton Inference Server e o DeepStream no Jetson são opções naturais para produção.

Armadilhas#

Passar uma lista Python de vídeos

Sintoma: model.track(["cam1.mp4", "cam2.mp4"]) trata a lista como um lote de imagens, não como fluxos. Solução: grave as origens em um arquivo .streams, uma por linha.

stream_buffer padrão em arquivos gravados

Sintoma: cada arquivo fornece apenas um ou dois quadros. Solução: stream_buffer=False descarta quadros obsoletos, e os leitores de arquivos avançam mais rápido que o modelo; defina stream_buffer=True para arquivos e mantenha o padrão para câmeras ao vivo.

Compartilhar um modelo entre threads de inferência

Sintoma: resultados incorretos ou misturados ocasionalmente sob carga. Solução: nunca chame uma única instância de YOLO simultaneamente de várias threads. Deixe um único consumidor ser responsável por ela, como acima, ou crie um modelo dentro de cada thread de inferência.

Enfileirar todos os quadros

Sintoma: a latência aumenta a cada minuto e a memória cresce até o processo encerrar. Solução: use uma fila limitada que descarte itens quando estiver cheia, além de um contador de descartes. Sistemas em tempo real devem descartar quadros durante sobrecargas, não ficar atrasados.

Experimente

Executa producer_consumer.py com as tuas próprias fontes (ou com os dois clipes de demonstração) e, depois, varia a quantidade de fluxos com o script de solução até surgirem descartes. Apresenta uma tabela com a quantidade de fluxos e a percentagem de descartes no teu hardware, bem como a maior quantidade de fluxos sem descartes no FPS pretendido.

Concluído quando
Você concluiu a aula quando todos estes critérios forem atendidos.
  • Dois ou mais fluxos são executados num único modelo e cada resultado é associado ao respetivo fluxo.

  • O teu pipeline usa uma fila limitada e informa os frames processados e descartados por fluxo; a soma corresponde aos frames lidos.

  • Consegues indicar, com base numa medição, a quantidade de fluxos a partir da qual o teu hardware começa a descartar frames.

Mostrar solução

Transforma a quantidade de fluxos num argumento da linha de comandos, adicionando estas linhas a producer_consumer.py, logo após a linha SOURCES:

import sys

N = int(sys.argv[1]) if len(sys.argv) > 1 else 2  # quantidade de câmeras simuladas
SOURCES = [SOURCES[i % len(SOURCES)] for i in range(N)]

Depois, varia o valor:

for n in 1 2 4 8; do python producer_consumer.py $n; done
processed=[62] dropped=[0]
processed=[56, 131] dropped=[6, 19]
processed=[23, 82, 23, 81] dropped=[39, 68, 39, 69]
processed=[14, 53, 13, 53, 13, 52, 13, 53] dropped=[48, 97, 49, 97, 49, 98, 49, 97]

A resposta para esta máquina é um fluxo: os descartes começam com dois. Para servir quatro câmeras, exporta para um runtime mais rápido (lições 3 e 4), reduz imgsz ou muda para um acelerador; depois, executa novamente os testes.

Próximos passos

Já conseguimos executar em escala. A seguir: monitorizar o sistema o suficiente para perceber quando o desempenho piora silenciosamente.

Entre para salvar seu progresso