Ultralytics YOLO27: Inscreve-te na lista de espera.
Get Started
Além da detecção: segmentação, pose, OBB e classificação·Classificar, combinar, embarcar·Aula 9/10
Aulaintermediário

Combinar tarefas em um pipeline

Crie uma cascata de detecção seguida de classificação, classifique cada objeto rastreado apenas uma vez em vez de a cada quadro e transforme os pontos-chave de pose em séries temporais por pessoa.

Os sistemas mais úteis além da detecção geralmente usam dois modelos, não um. Um detector encontra cada trabalhador e um classificador decide se cada capacete é do tipo correto; um modelo de pose encontra as articulações e um rastreador as transforma em uma linha do tempo a partir da qual você pode contar repetições. O código é curto. O que costuma dar errado nesses sistemas é um conjunto de dados da segunda etapa que não corresponde aos recortes da primeira etapa e um custo que se multiplica por cada objeto em cada quadro — executado sem otimização, um fluxo de 30 FPS com dez pessoas custa 300 classificações por segundo.

Resultado

Execute um script de detecção seguida de classificação, uma versão que classifica cada pessoa rastreada apenas uma vez e um rastreador de pose que mantém os ângulos dos joelhos por pessoa; crie os dados de treinamento do classificador a partir dos próprios recortes do detector.

Via rápida
Se já sabes orientar-te, aqui está a versão resumida.
  1. Detecte, recorte com result.orig_img[y1:y2, x1:x2] e classifique todos os recortes em uma única chamada em lote.

  2. Crie o conjunto de dados do classificador a partir dos recortes do seu detector (save_crop=True), não de fotos de estúdio.

  3. Em vídeo, rastreie com persist=True e classifique cada result.boxes.id uma única vez; depois, reutilize o rótulo.

  4. Para combinar pose e rastreamento, indexe o histórico de pontos-chave de cada pessoa pelo ID de rastreamento.

Prático

Dois padrões que atendem à maioria dos produtos#

Ambos começam com o mesmo quadro e a mesma API da Ultralytics; diferem no que a segunda etapa consome. A seleção e o ajuste do rastreador, assim como o tratamento do fluxo para o rastreamento de objetos, fazem parte de Rastrear objetos com ByteTrack, BoT-SORT e TrackTrack; aqui, o foco é combinar as tarefas.

Detectar e, em seguida, classificar os recortes#

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5)[0]
crops, labels = [], []
for box in result.boxes:
    x1, y1, x2, y2 = box.xyxy[0].int().tolist()
    crops.append(result.orig_img[y1:y2, x1:x2])  # BGR numpy crop, same as the detector saw
    labels.append(result.names[int(box.cls)])

for label, crop_result in zip(labels, classifier(crops, verbose=False)):  # one batched call for all crops
    probs = crop_result.probs
    print(f"{label:6s} -> {crop_result.names[probs.top1]} ({float(probs.top1conf):.2f})")
bus    -> bullet_train (0.31)
person -> sweatshirt (0.24)
person -> abaya (0.50)
person -> loudspeaker (0.49)
person -> frilled_lizard (0.24)

A integração está correta: os recortes vêm de orig_img na resolução original, nas coordenadas de pixels do próprio detector, e o classificador os processa todos em uma única chamada em lote. Os rótulos são absurdos de propósito.

A qualidade de uma segunda etapa depende de sua correspondência com os recortes

O classificador ImageNet nunca foi treinado com figuras de rua muito recortadas, parcialmente cortadas, então ele faz suposições — observe que nenhuma confiança é superior a 0.50. O mesmo acontece com um classificador treinado com fotos nítidas de produtos que recebe recortes borrados do detector.

Criar o conjunto de dados da segunda etapa a partir dos seus próprios recortes#

A maneira confiável de obter dados correspondentes é deixar o detector gerá-los:

Grade de recortes numerados de malas, salvos pelo Ultralytics YOLO26 a partir de imagens de esteiras de aeroporto, prontos para serem organizados em pastas do classificador

  1. Execute o detector em imagens representativas com save_crop=True, por exemplo, yolo predict model=best.pt source=footage/ save_crop=True. Os recortes são salvos em runs/detect/predict/crops/<class>/.
  2. Organize os recortes de cada classe nas pastas de classificação detalhada (helmet_ok/, helmet_missing/) — essa já é a estrutura de classificação da lição anterior.
  3. Treine o classificador nessas pastas e valide-o com recortes de outro dia ou outra câmera.

Como o detector extraiu os recortes, eles mantêm o enquadramento, o desfoque e as oclusões reais — exatamente o que o classificador verá em produção — e o ajuste fino neles é um exemplo comum de aprendizado por transferência.

Classificar uma vez por ID de rastreamento#

Um capacete não muda de um quadro para outro, então classificar cada caixa em cada quadro é trabalho desperdiçado. Rastreie as detecções e classifique cada novo ID de rastreamento uma única vez:

from ultralytics import YOLO

detector = YOLO("yolo26n.pt")
classifier = YOLO("yolo26n-cls.pt")  # swap in your own fine-tuned classifier
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"

labels = {}  # track id -> (label, confidence), filled once per person
frames = boxes_seen = 0
for result in detector.track(source, classes=[0], stream=True, persist=True, verbose=False):  # people only
    frames += 1
    if result.boxes.id is None:
        continue
    for track_id, (x1, y1, x2, y2) in zip(result.boxes.id.int().tolist(), result.boxes.xyxy.int().tolist()):
        boxes_seen += 1
        if track_id not in labels:  # first sighting: classify once, then reuse
            probs = classifier(result.orig_img[y1:y2, x1:x2], verbose=False)[0].probs
            labels[track_id] = (classifier.names[probs.top1], round(float(probs.top1conf), 2))

print(f"{frames} frames, {boxes_seen} person boxes, {len(labels)} classifier calls")
print(labels)
11 frames, 33 person boxes, 3 classifier calls
{1: ('basketball', 0.51), 2: ('maillot_(tights)', 0.14), 3: ('bulletproof_vest', 0.23)}

Três chamadas ao classificador, em vez de 33, em um clipe de 11 quadros; em um fluxo longo, a economia aumenta a cada quadro em que uma pessoa permanece visível. Os rótulos ainda são previsões do ImageNet — substitua-as pelo seu classificador ajustado e elas se tornarão as respostas para cada pessoa.

Regra de decisão

Classifique uma vez por trajetória quando o atributo não puder mudar enquanto o objeto estiver visível (tipo de capacete, marca do veículo, SKU do produto). Reclassifique a cada N quadros quando puder mudar (porta aberta, mão levantada). Quando as primeiras aparições tendem a ser pequenas e borradas, classifique no quadro em que a caixa da trajetória é maior.

Pose e rastreamento#

Estimativa de pose com Ultralytics YOLO contando flexões, com o esqueleto corporal, um ângulo de cotovelo de 110 graus e um contador de repetições

O modo Track atribui um result.boxes.id persistente a cada pessoa, transformando os pontos-chave em uma série temporal. Este script registra o ângulo do joelho esquerdo de cada pessoa ao longo do mesmo vídeo curto:

from collections import defaultdict

import numpy as np

from ultralytics import YOLO

def angle(a, b, c):
    """Angle at b, in degrees, formed by points a-b-c."""
    ba, bc = a - b, c - b
    return np.degrees(np.arccos(np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))))

model = YOLO("yolo26n-pose.pt")
source = "https://github.com/ultralytics/assets/releases/download/v0.0.0/decelera_portrait_min.mov"
HIP, KNEE, ANKLE = 11, 13, 15  # left leg, COCO keypoint order
knee_angles = defaultdict(list)  # track id -> knee angle per frame

for result in model.track(source, stream=True, persist=True, verbose=False):
    if result.boxes.id is None:  # no confirmed tracks in this frame
        continue
    kpts = result.keypoints.cpu().numpy()
    for track_id, xy, conf in zip(result.boxes.id.int().tolist(), kpts.xy, kpts.conf):
        if conf[[HIP, KNEE, ANKLE]].min() > 0.5:  # skip occluded legs
            knee_angles[track_id].append(angle(xy[HIP], xy[KNEE], xy[ANKLE]))

for track_id, angles in sorted(knee_angles.items()):
    print(f"id {track_id}: {len(angles)} frames, left knee {min(angles):.0f}-{max(angles):.0f} deg")
id 1: 11 frames, left knee 170-176 deg
id 2: 11 frames, left knee 96-118 deg
id 3: 11 frames, left knee 177-180 deg

O joelho dobrado da ID 2 corresponde à pessoa sentada no clipe; as outras duas estão de pé, com as pernas esticadas, perto de 180°. A partir daqui, um contador de repetições verifica se a série de cada ID cruza determinados limites — desce abaixo de um ângulo e volta a subir acima de outro —, algo que a solução de monitoramento de treinos oferece pronta para uso.

Calcular o custo do pipeline#

Fator de custoDetectar e depois classificarPose e rastreamento
Escala comobjetos por quadroquadros
Principal alavancarecorta em lote; classifica uma vez por ID de rastreamento e armazena em cachereduz imgsz ou vid_stride quando o movimento é lento
Modo de falhao classificador recebe recortes diferentes dos dados de treinoas trocas de ID dividem o histórico de uma pessoa em dois
Trocas de ID interrompem os históricos

Sintoma: um contador de repetições zera no meio da série, ou uma pessoa aparece com dois IDs. Causa: o rastreador perdeu a pessoa (oclusão, uma virada rápida) e atribuiu um novo ID. Solução: ajuste o rastreador conforme o curso de produção e faça com que a lógica por ID tolere lacunas curtas — um rótulo de classificador em cache simplesmente é recalculado para o novo ID.

Experimente

Executa two_stage.py com save_crop=True adicionado à chamada do detector e confirma que os recortes aparecem em pastas por classe. Executa classify_per_track.py e compara as chamadas do classificador com as caixas das pessoas. Depois, amplia pose_track.py para imprimir, por ID de rastreamento, quantos frames têm um ângulo do joelho inferior a 120°.

Concluído quando
Você concluiu a aula quando todos estes critérios forem atendidos.
  • Os recortes estão em crops/<class>/, e consegues descrever como os organizarias em pastas para o classificador.

  • Consegues dizer quantas chamadas do classificador o armazenamento em cache por ID de rastreamento poupou no clipe de exemplo.

  • pose_track.py imprime uma linha por ID de rastreamento com a contagem de frames e a contagem de ângulos inferiores a 120°.

Mostrar solução

Adiciona save_crop=True à chamada do detector:

result = detector("https://ultralytics.com/images/bus.jpg", conf=0.5, save_crop=True)[0]

Os recortes aparecem em runs/detect/predict*/crops/person/ e crops/bus/ (o nome da pasta da execução aumenta a cada execução de predição). O armazenamento em cache por ID de rastreamento fez 3 chamadas ao classificador para 33 caixas de pessoas, poupando 30 chamadas num clipe de 11 frames.

Para contar os ângulos do joelho, substitui o ciclo final de pose_track.py:

for track_id, angles in sorted(knee_angles.items()):
    bent = sum(a < 120 for a in angles)
    print(f"id {track_id}: {len(angles)} frames, {bent} with knee below 120 deg")
id 1: 11 frames, 0 with knee below 120 deg
id 2: 11 frames, 11 with knee below 120 deg
id 3: 11 frames, 0 with knee below 120 deg

O ID 2, correspondente à pessoa sentada, está abaixo de 120° em todos os 11 frames; os IDs 1 e 3 nunca estão.

Próximos passos

Por fim, avaliamos cada tarefa com a métrica adequada e exportamos cada modelo para implantação.

Entre para salvar seu progresso