Reconhecimento de Objetos com YOLO: do Código à Matemática
Introdução
Apontar uma câmera para uma rua e receber de volta, em milissegundos, retângulos rotulados sobre cada carro, pedestre e semáforo parece mágica. Não é: é uma rede neural convolucional treinada para transformar uma grade de pixels em uma lista de objetos com posição, tamanho e nome.
Neste artigo vamos dissecar um detector de objetos funcional, pouco mais de cem linhas de Python que carregam o modelo YOLOv8, recebem uma imagem enviada pelo usuário, detectam até 80 categorias de objetos, desenham as caixas delimitadoras e devolvem tudo traduzido para o português. Primeiro entendemos o código por completo; depois mergulhamos na matemática que faz o reconhecimento funcionar: como uma convolução enxerga bordas, como a rede regride coordenadas de caixas, o que significa a "confiança" de uma detecção e por que o Non-Maximum Suppression é indispensável.
Em uma frase: o YOLO ("You Only Look Once") divide a imagem em uma grade, prevê caixas e probabilidades de classe para cada célula em uma única passagem da rede, e filtra o resultado com IoU e supressão de não-máximos.
Detecção de objetos: o problema
Classificação de imagens responde "o que é esta foto?" e devolve um único rótulo. Detecção de objetos é mais ambiciosa: responde "quais objetos existem e onde cada um está?". A saída não é um rótulo, mas uma lista de tuplas:
onde é a categoria, é a confiança e descrevem a caixa delimitadora (bounding box): centro, largura e altura. O modelo que usaremos foi treinado no dataset COCO, que define 80 classes do cotidiano, de "pessoa" a "escova de dente".
Visão geral do código
O programa tem quatro responsabilidades bem separadas:
- Tradução: um dicionário que mapeia as 80 classes em inglês do COCO para o português.
- Carregar o modelo: baixar e instanciar o YOLOv8n uma única vez.
- Reconhecer: abrir a imagem, rodar a inferência, ordenar resultados e desenhar as caixas.
- Interface: um widget de upload no Jupyter/Colab que dispara tudo.
Vamos por partes.
Parte 1: traduzindo as classes do COCO
O YOLO devolve rótulos em inglês (person, dog, car...). Como o público é brasileiro, um dicionário faz a ponte:
TRADUCAO = {
"person": "Pessoa", "bicycle": "Bicicleta", "car": "Carro",
"dog": "Cachorro", "cat": "Gato", "cell phone": "Celular",
# ... as 80 classes COCO
"toothbrush": "Escova de dente",
}
Não há matemática aqui, apenas uma escolha de produto: a inteligência é universal, mas a apresentação é localizada. O detalhe importante aparece mais adiante, no acesso seguro TRADUCAO.get(classe_en, classe_en.capitalize()): se uma classe não estiver no dicionário, usamos o nome original capitalizado em vez de quebrar.
Parte 2: carregando o modelo
def carregar_modelo():
from ultralytics import YOLO
print("⏳ Carregando modelo YOLOv8n (baixa ~6MB na 1ª vez)...")
modelo = YOLO("yolov8n.pt")
print("✅ Modelo pronto!\n")
return modelo
A biblioteca ultralytics encapsula toda a complexidade. A string "yolov8n.pt" pede a variante nano, a menor da família YOLOv8, com cerca de 3,2 milhões de parâmetros e apenas ~6 MB. Na primeira execução o arquivo de pesos é baixado; depois fica em cache.
O sufixo n (nano) é uma escolha de trade-off. Existem n, s, m, l e x, em ordem crescente de tamanho. O nano roda em CPU em tempo quase real, ao custo de errar mais objetos pequenos. Para um app de upload interativo, é a escolha certa: latência baixa vence acurácia de ponta.
Por que carregar dentro da função e só uma vez: instanciar a rede e mover pesos para memória é caro. No código, a variável global
modelocomeça comoNonee só é preenchida no primeiro clique, o lazy loading. Cliques seguintes reutilizam o modelo já carregado.
Parte 3: o coração, a função reconhecer
Aqui acontece o trabalho de verdade. Vamos quebrá-la em blocos.
3.1 Abrindo e convertendo a imagem
pil = Image.open(BytesIO(imagem_bytes)).convert("RGB")
bgr = np.array(pil)[:, :, ::-1].copy()
print(f"📐 Tamanho: {pil.width}×{pil.height} px")
A imagem chega como bytes crus do upload. BytesIO a transforma em um arquivo em memória, e o Pillow a decodifica. O .convert("RGB") garante três canais (descartando transparência de PNGs, por exemplo).
A linha mais críptica é [:, :, ::-1]. Uma imagem é um tensor de forma : altura, largura e três canais de cor. O Pillow usa a ordem RGB; o OpenCV, por razões históricas, usa BGR. O slice ::-1 no último eixo inverte a ordem dos canais, convertendo RGB → BGR. O .copy() é necessário porque o slice invertido gera uma view com stride negativo, que o OpenCV recusa.
Matematicamente, isso é só uma permutação dos canais. Se é o pixel RGB, aplicamos a matriz de permutação:
3.2 Rodando a inferência
resultados = modelo(bgr, conf=0.35, verbose=False)
Esta única linha dispara a passagem direta (forward pass) pela rede. O parâmetro conf=0.35 é o limiar de confiança: detecções abaixo de 35% são descartadas antes mesmo de chegarem a você. Esse número é uma alavanca de precisão/revocação: subi-lo reduz falsos positivos, mas faz perder objetos sutis.
3.3 Extraindo as detecções
objetos = []
for r in resultados:
for box in r.boxes:
classe_en = r.names[int(box.cls)]
objetos.append({
"nome": TRADUCAO.get(classe_en, classe_en.capitalize()),
"original": classe_en,
"confianca": round(float(box.conf), 3),
"bbox": list(map(int, box.xyxy[0].tolist())),
})
objetos.sort(key=lambda x: -x["confianca"])
Cada box carrega três informações que correspondem exatamente à saída matemática do modelo:
box.cls: o índice da classe prevista (0 a 79), convertido em nome viar.names.box.conf: a confiança .box.xyxy: a caixa no formato : cantos superior-esquerdo e inferior-direito, em pixels.
Note que o YOLO internamente trabalha com (centro + dimensões), mas a API expõe xyxy (cantos) por ser mais conveniente para desenhar. A conversão é direta:
Por fim, objetos.sort(key=lambda x: -x["confianca"]) ordena do mais confiante ao menos confiante, para que o "objeto principal" seja o primeiro.
3.4 Relatório no terminal
if not objetos:
print("⚠️ Nenhum objeto identificado.")
else:
print(f"🏆 OBJETO PRINCIPAL: {objetos[0]['nome'].upper()}")
print(f" Confiança: {objetos[0]['confianca']:.0%}")
for o in objetos[1:]:
print(f" • {o['nome']} ({o['confianca']:.0%})")
Pura apresentação: o objeto de maior confiança vira destaque e os demais formam uma lista. O formato :.0% transforma 0.873 em 87%.
3.5 Desenhando as caixas delimitadoras
CORES = [(220,80,50),(50,190,120),(40,130,220),(200,60,180),(60,200,200)]
img_out = bgr.copy()
for i, obj in enumerate(objetos):
cor = CORES[i % len(CORES)]
x1, y1, x2, y2 = obj["bbox"]
label = f"{obj['nome']} {obj['confianca']:.0%}"
cv2.rectangle(img_out, (x1,y1), (x2,y2), cor, 2)
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1)
cv2.rectangle(img_out, (x1, y1-th-10), (x1+tw+8, y1), cor, -1)
cv2.putText(img_out, label, (x1+4, y1-5),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,255,255), 1, cv2.LINE_AA)
Para cada objeto: escolhe-se uma cor cíclica (i % len(CORES) evita estourar a lista), desenha-se o retângulo da caixa, mede-se o tamanho do texto com getTextSize, pinta-se um retângulo preenchido de fundo (-1 significa preenchido) e escreve-se o rótulo branco por cima. O cálculo y1-th-10 posiciona o fundo do texto acima da caixa, evitando cobrir o objeto.
3.6 Salvando e exibindo
_, buf = cv2.imencode(".jpg", img_out)
jpg_bytes = buf.tobytes()
saida = f"{Path(nome_arquivo).stem}_detectado.jpg"
with open(saida, "wb") as f:
f.write(jpg_bytes)
display(IPImage(data=jpg_bytes, width=700))
print(f"💾 Salvo em: {saida}")
cv2.imencode comprime o array NumPy de volta para JPEG em memória. Esses bytes servem tanto para gravar o arquivo _detectado.jpg quanto para exibir inline no notebook via IPImage.
Parte 4: a interface de upload
upload = widgets.FileUpload(accept="image/*", multiple=False)
botao = widgets.Button(description="🔍 Reconhecer", button_style="primary")
saida_widget = widgets.Output()
modelo = None # carregado ao clicar
def ao_clicar(b):
global modelo
with saida_widget:
saida_widget.clear_output()
if not upload.value:
print("⚠️ Nenhuma imagem selecionada.")
return
if modelo is None:
modelo = carregar_modelo()
arquivo = list(upload.value.values())[0]
nome = list(upload.value.keys())[0]
reconhecer(modelo, arquivo["content"], nome)
botao.on_click(ao_clicar)
display(widgets.VBox([upload, botao, saida_widget]))
ipywidgets constrói uma mini-interface reativa dentro do notebook: um campo de upload, um botão e uma área de saída. O with saida_widget: redireciona todos os print e display para dentro do widget, mantendo a saída organizada. O clear_output() limpa o resultado anterior a cada novo clique. E o lazy loading do modelo aparece de novo: ele só carrega na primeira detecção.
A matemática por trás do reconhecimento
Agora a parte que realmente importa. Aquele modelo(bgr) esconde uma rede neural convolucional profunda. Vamos abrir a caixa-preta.
A convolução: como a rede enxerga
A imagem é um tensor . A operação fundamental que extrai padrões dela é a convolução. Um kernel (ou filtro) de tamanho desliza sobre a imagem, e em cada posição calcula uma soma ponderada da vizinhança:
O resultado é um mapa de ativação. O segredo é que os valores do kernel não são programados: são aprendidos no treino. Um filtro pode convergir para detectar bordas verticais, outro para texturas, outro para tons de pele. Empilhando camadas, a rede compõe padrões simples em conceitos complexos: bordas → cantos → rodas → carros.
Para dar uma intuição, o kernel de Sobel (fixo, clássico) detecta bordas verticais:
Onde há transição abrupta de claro para escuro na horizontal, a soma ponderada dispara um valor alto; em regiões uniformes, dá zero. As CNNs aprendem milhares de filtros como esse, mas ajustados aos dados.
Após cada convolução vem uma não-linearidade, tipicamente a ReLU, que descarta ativações negativas:
Sem ela, empilhar camadas seria inútil: a composição de funções lineares continua linear. A não-linearidade é o que permite à rede aprender fronteiras de decisão curvas e arbitrariamente complexas.
A grade do YOLO: detecção em uma passagem
O nome You Only Look Once descreve a ideia central. Em vez de varrer a imagem com milhares de janelas candidatas (como faziam detectores antigos), o YOLO processa a imagem inteira de uma só vez e a divide conceitualmente em uma grade .
Cada célula da grade é responsável por prever objetos cujo centro caia dentro dela. Para cada célula, a rede produz um vetor com:
- Caixas delimitadoras: cada uma com e uma confiança de objeto.
- Probabilidades de classe: a distribuição sobre as classes (80 no COCO).
A confiança de objeto de uma caixa é definida como:
Ou seja, ela combina duas coisas: a probabilidade de existir um objeto ali e o quão bem a caixa prevista se alinha à caixa real. Uma caixa pode "saber" que há um objeto, mas se estiver mal posicionada, a confiança cai.
A pontuação final de classe específica, aquela que vira o box.conf do código, é o produto da confiança de objeto pela probabilidade condicional da classe:
É por isso que conf=0.35 filtra tanto detecções incertas quanto caixas mal alinhadas de uma vez só.
Regressão das coordenadas da caixa
Como a rede prevê posições? Ela não chuta pixels diretamente, pois isso seria instável. Em vez disso, prevê deslocamentos relativos à célula da grade e os passa por funções que limitam o intervalo. Para o centro:
onde são as saídas cruas da rede, é a sigmoide (que comprime o valor para , mantendo o centro dentro da célula) e é o canto da célula da grade. A sigmoide:
Para as dimensões, versões clássicas do YOLO usam uma exponencial sobre âncoras de referência :
A exponencial garante largura e altura sempre positivas, e prever um fator multiplicativo sobre uma âncora é mais fácil de aprender do que prever a dimensão absoluta do zero. Versões modernas como o YOLOv8 são anchor-free e regridem distâncias do centro às quatro bordas, mas o princípio (prever deslocamentos limitados, não coordenadas absolutas) permanece.
IoU: medindo a qualidade de uma caixa
A Interseção sobre União (Intersection over Union) é a métrica que quantifica o quanto duas caixas se sobrepõem. Para uma caixa prevista e a real :
O valor vai de (sem sobreposição) a (caixas idênticas). Ela aparece em dois momentos: no treino, definindo quão boa é uma previsão, e na inferência, dentro do passo de supressão a seguir.
Non-Maximum Suppression: removendo caixas duplicadas
Aqui está um problema prático: a rede frequentemente dispara várias caixas para o mesmo objeto, vindas de células vizinhas. Sem tratamento, você veria três retângulos sobrepostos no mesmo cachorro. O Non-Maximum Suppression (NMS) resolve isso:
- Ordene todas as caixas pela confiança (do código:
objetos.sort(...)). - Pegue a de maior confiança e marque-a como definitiva.
- Remova todas as outras caixas cujo IoU com ela exceda um limiar (ex.: ), pois são duplicatas do mesmo objeto.
- Repita com a próxima caixa restante de maior confiança.
Em pseudocódigo:
def nms(caixas, limiar_iou=0.45):
caixas = sorted(caixas, key=lambda b: -b.conf)
mantidas = []
while caixas:
melhor = caixas.pop(0) # maior confiança
mantidas.append(melhor)
# descarta as que se sobrepõem muito à melhor
caixas = [b for b in caixas if iou(melhor, b) < limiar_iou]
return mantidas
O ultralytics faz isso por baixo dos panos antes de devolver r.boxes, por isso o código do app não precisa implementar NMS. Mas é justamente este passo que garante uma caixa por objeto.
A função de perda: o que o treino otimiza
Tudo isso é aprendido minimizando uma função de perda que pune três tipos de erro simultaneamente. Em forma simplificada:
Cada termo cuida de um aspecto:
- Localização: erro nas coordenadas , hoje tipicamente medido por perdas baseadas em IoU (como CIoU), que otimizam diretamente a sobreposição.
- Confiança: quão bem a rede estima a presença de objeto, via entropia cruzada binária.
- Classe: erro na distribuição de classes, também por entropia cruzada.
A entropia cruzada, base dos dois últimos termos, para uma previsão e alvo , é:
Os pesos equilibram as prioridades, e geralmente acertar a localização recebe peso maior. O treino ajusta milhões de parâmetros por gradiente descendente e retropropagação, repetindo isso por milhões de imagens anotadas até a perda estabilizar.
Juntando tudo: o caminho de um pixel ao rótulo
Quando você clica em "🔍 Reconhecer", esta é a jornada:
- A imagem é redimensionada e normalizada para a entrada da rede.
- Dezenas de camadas convolucionais extraem mapas de ativação cada vez mais abstratos.
- A cabeça de detecção produz, para cada célula da grade, caixas , confiança e probabilidades de classe.
- O limiar
conf=0.35corta detecções fracas. - O NMS elimina caixas duplicadas via IoU.
- As caixas sobreviventes são convertidas para coordenadas de pixel
xyxy. - O código traduz, ordena, desenha e exibe.
Toda a matemática (convoluções, sigmoides, exponenciais, IoU, entropia cruzada) colapsa naquela única chamada modelo(bgr).
Exemplo de saída
Para uma foto de sala de estar, a saída típica seria:
📐 Tamanho: 1280×853 px
🔍 Detectando objetos...
─────────────────────────────────────────────
🏆 OBJETO PRINCIPAL: SOFÁ
Confiança: 91%
📋 Outros objetos detectados:
• TV (88%)
• Planta em vaso (74%)
• Controle remoto (52%)
• Livro (41%)
─────────────────────────────────────────────
🖼️ Imagem com anotações:
💾 Salvo em: sala_detectado.jpg
Cada percentual é o da seção de matemática: produto de probabilidade de objeto, IoU e probabilidade de classe.
Conclusão
Um detector de objetos moderno comprime décadas de pesquisa em visão computacional numa API de uma linha. Mas por trás de modelo(bgr) há uma estrutura matemática elegante e bem definida: convoluções aprendem a enxergar padrões, uma grade divide a responsabilidade espacial, sigmoides e exponenciais regridem caixas estáveis, o IoU mede qualidade, o NMS remove redundância e uma função de perda multi-termo orquestra todo o aprendizado.
O código que percorremos é, no fundo, uma fina camada de conveniência (tradução, desenho e interface) sobre esse núcleo. Entender a matemática não é exigência para usar a ferramenta, mas é o que separa quem aperta o botão de quem sabe por que ele falha em objetos pequenos, por que conf é uma alavanca de precisão, e como ajustá-la quando o problema real bater à porta.
Próximos passos: experimente trocar
yolov8n.ptporyolov8s.pte compare acurácia e latência; varie o limiarconfe observe o equilíbrio entre falsos positivos e objetos perdidos; e estude a cabeça de detecção anchor-free do YOLOv8 para entender a regressão de distâncias às bordas.
Referências: Redmon et al., You Only Look Once: Unified, Real-Time Object Detection (CVPR 2016); Lin et al., Microsoft COCO: Common Objects in Context (2014); documentação do Ultralytics YOLOv8; Goodfellow, Bengio e Courville, Deep Learning (2016, Cap. 9: Redes Convolucionais).
Gostou deste artigo? Inscreva-se na newsletter.