Detecção de Objetos com YOLO: Encontrando Objetos em Imagens e Vídeos
A detecção de objetos encontra quais objetos existem em uma imagem e onde estão. A saída são caixas delimitadoras com o nome da classe e uma pontuação de confiança. A família de modelos YOLO é rápida o bastante para vídeo. Este artigo explica as ideias por trás disso: coordenadas das caixas, IoU, NMS, decodificação das células da grade, redimensionamento com letterbox e a medição de qualidade com precisão, recall e AP. Cada exemplo é JavaScript que usa apenas a biblioteca padrão, então pode ser executado sem baixar nenhum modelo.
O que você precisa
Você precisa de Node.js 18 ou mais recente e de conhecimento básico de JavaScript. Execute cada exemplo com node nomedoarquivo.js. Não são necessários pacotes adicionais. Executar um modelo YOLO de verdade sobre imagens exige Python e a biblioteca ultralytics. Isso não foi executado na máquina de teste deste artigo, por isso aparece apenas na seção de próximos passos.
Ilustração
Exemplo 1: Uma imagem é uma matriz de números
Uma imagem digital é uma grade de valores de pixel, então o modelo trabalha com números. Este exemplo cria uma imagem preto e branco 4x4 e conta os pixels claros.
// Imagem preto e branco 4x4 guardada como matriz de valores de brilho de 0 a 255
const image = [
[0, 0, 255, 255],
[0, 0, 255, 255],
[0, 0, 0, 0],
[0, 0, 0, 0],
];
const height = image.length;
const width = image[0].length;
console.log(`Tamanho da imagem: largura ${width} x altura ${height}`);
let bright = 0;
for (const row of image) for (const p of row) if (p > 127) bright++;
console.log("Quantidade de pixels claros:", bright);
console.log("Valor do pixel do canto superior esquerdo:", image[0][0]);
Saída real
Tamanho da imagem: largura 4 x altura 4
Quantidade de pixels claros: 4
Valor do pixel do canto superior esquerdo: 0
Exemplo 2: Convertendo o formato da caixa
O YOLO devolve o centro com largura e altura (cx, cy, w, h). Para desenhar e calcular IoU, normalmente são necessários o canto superior esquerdo e o canto inferior direito (x1, y1, x2, y2).
// O modelo YOLO devolve a caixa como (cx, cy, w, h), mas para desenhar e calcular costuma-se usar (x1, y1, x2, y2)
function xywhToXyxy([cx, cy, w, h]) {
return [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2];
}
const box = [320, 240, 100, 50];
const [x1, y1, x2, y2] = xywhToXyxy(box);
console.log("Canto superior esquerdo:", x1, y1);
console.log("Canto inferior direito:", x2, y2);
console.log("largura:", x2 - x1, "altura:", y2 - y1);
Saída real
Canto superior esquerdo: 270 215
Canto inferior direito: 370 265
largura: 100 altura: 50
Exemplo 3: IoU mede a sobreposição das caixas
IoU é a área sobreposta dividida pela área combinada. O valor vai de 0 a 1, e 1 significa que as duas caixas são idênticas.
// IoU = área sobreposta / área combinada das duas caixas
function area([x1, y1, x2, y2]) {
return Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
}
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]);
const iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]);
const iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / (area(a) + area(b) - inter);
}
const a = [0, 0, 100, 100];
console.log("Caixas iguais:", iou(a, a).toFixed(2));
console.log("Sobreposição de um terço:", iou(a, [50, 0, 150, 100]).toFixed(2));
console.log("Sem sobreposição:", iou(a, [200, 200, 300, 300]).toFixed(2));
Saída real
Caixas iguais: 1.00
Sobreposição de um terço: 0.33
Sem sobreposição: 0.00
Exemplo 4: Filtrando pela confiança
O modelo prevê muitas caixas. As caixas com confiança abaixo de um limite são descartadas antes da próxima etapa.
// Descarte primeiro as caixas de baixa confiança, usando um limite de confiança (confidence threshold)
const detections = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "car", score: 0.31, box: [200, 70, 260, 130] },
{ label: "person", score: 0.88, box: [300, 50, 340, 170] },
{ label: "dog", score: 0.12, box: [10, 150, 60, 200] },
];
const THRESHOLD = 0.5;
const kept = detections.filter(d => d.score >= THRESHOLD);
console.log(`Restam ${kept.length} de ${detections.length} caixas (limite ${THRESHOLD})`);
for (const d of kept) console.log(d.label, d.score.toFixed(2));
Saída real
Restam 2 de 4 caixas (limite 0.5)
car 0.94
person 0.88
Exemplo 5: Non-Maximum Suppression (NMS)
O NMS ordena as caixas pela confiança, guarda a melhor e descarta as outras caixas com IoU alto em relação a ela. O resultado é uma caixa para cada objeto.
// Non-Maximum Suppression: guarde a caixa mais confiante e descarte as caixas que se sobrepõem muito a ela
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
const areaA = (a[2] - a[0]) * (a[3] - a[1]);
const areaB = (b[2] - b[0]) * (b[3] - b[1]);
return inter / (areaA + areaB - inter);
}
function nms(dets, iouThreshold) {
const sorted = [...dets].sort((p, q) => q.score - p.score);
const keep = [];
for (const d of sorted) {
if (keep.every(k => iou(k.box, d.box) < iouThreshold)) keep.push(d);
}
return keep;
}
const dets = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "car", score: 0.81, box: [45, 62, 185, 142] },
{ label: "car", score: 0.67, box: [200, 70, 260, 130] },
{ label: "person", score: 0.88, box: [300, 50, 340, 170] },
];
const result = nms(dets, 0.5);
console.log(`Antes do NMS ${dets.length} caixas -> depois do NMS ${result.length} caixas`);
for (const d of result) console.log(d.label, d.score.toFixed(2));
Saída real
Antes do NMS 4 caixas -> depois do NMS 3 caixas
car 0.94
person 0.88
car 0.67
Exemplo 6: NMS por classe
Se o NMS misturar todas as classes, um carro e um cachorro sobrepostos podem perder uma das caixas. Execute o NMS separadamente para cada classe.
// NMS por classe: caixas de classes diferentes que se sobrepõem são mantidas as duas
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
function nms(dets, iouThreshold) {
const sorted = [...dets].sort((p, q) => q.score - p.score);
const keep = [];
for (const d of sorted) if (keep.every(k => iou(k.box, d.box) < iouThreshold)) keep.push(d);
return keep;
}
function nmsPerClass(dets, iouThreshold) {
const groups = {};
for (const d of dets) (groups[d.label] = groups[d.label] || []).push(d);
return Object.values(groups).flatMap(g => nms(g, iouThreshold));
}
const dets = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "dog", score: 0.90, box: [50, 65, 170, 150] },
{ label: "car", score: 0.85, box: [45, 62, 185, 142] },
];
const result = nmsPerClass(dets, 0.5);
console.log("Depois do NMS por classe:");
for (const d of result) console.log(d.label, d.score.toFixed(2));
Saída real
Depois do NMS por classe:
car 0.94
dog 0.90
Exemplo 7: Decodificando as células da grade
O YOLO divide a imagem em uma grade, e cada célula prevê valores brutos. Aplique sigmoid ao centro e exp à largura e à altura para obter as coordenadas em pixels.
// O YOLO divide a imagem em uma grade, e cada célula gera uma caixa em valores brutos. Esses valores precisam ser transformados com sigmoid e exp
const sigmoid = x => 1 / (1 + Math.exp(-x));
const STRIDE = 100; // imagem 400x400 dividida em 4x4 células, cada célula com 100 pixels
const raw = [
{ name: "Célula A", row: 1, col: 2, tx: 0.2, ty: -0.4, tw: 0.0, th: 0.5, obj: 3.0 },
{ name: "Célula B", row: 3, col: 0, tx: -1.5, ty: 1.0, tw: -0.3, th: 0.1, obj: -2.0 },
];
for (const r of raw) {
const cx = (r.col + sigmoid(r.tx)) * STRIDE;
const cy = (r.row + sigmoid(r.ty)) * STRIDE;
const w = Math.exp(r.tw) * STRIDE;
const h = Math.exp(r.th) * STRIDE;
const conf = sigmoid(r.obj);
console.log(`${r.name}: cx=${cx.toFixed(1)} cy=${cy.toFixed(1)} w=${w.toFixed(1)} h=${h.toFixed(1)} conf=${conf.toFixed(2)}`);
}
Saída real
Célula A: cx=255.0 cy=140.1 w=100.0 h=164.9 conf=0.95
Célula B: cx=18.2 cy=373.1 w=74.1 h=110.5 conf=0.12
Exemplo 8: Softmax escolhe a classe
As pontuações brutas de cada classe são transformadas em probabilidades que somam 100 por cento. A de maior valor é a previsão.
// Transforme as pontuações brutas de cada classe em probabilidades com softmax, depois escolha a classe mais alta
const names = ["person", "car", "dog"];
const logits = [1.2, 3.1, 0.4];
const exps = logits.map(Math.exp);
const sum = exps.reduce((a, b) => a + b, 0);
const probs = exps.map(e => e / sum);
probs.forEach((p, i) => console.log(names[i].padEnd(8), (p * 100).toFixed(1) + "%"));
const best = probs.indexOf(Math.max(...probs));
console.log("Previsão:", names[best]);
Saída real
person 12.3%
car 82.2%
dog 5.5%
Previsão: car
Exemplo 9: Letterbox e mapeamento de volta
O modelo recebe um tamanho fixo, como 640x640, então a imagem é reduzida mantendo a proporção e recebe bordas de preenchimento. Depois da detecção, o preenchimento é removido e os valores são divididos pela escala para voltar ao tamanho original.
// O modelo recebe uma imagem de tamanho fixo 640x640, então a imagem é reduzida mantendo a proporção e recebe bordas pretas (letterbox)
// As coordenadas das caixas também precisam ser convertidas de volta para o tamanho da imagem original
const orig = { w: 1280, h: 720 };
const INPUT = 640;
const scale = Math.min(INPUT / orig.w, INPUT / orig.h);
const newW = Math.round(orig.w * scale);
const newH = Math.round(orig.h * scale);
const padX = (INPUT - newW) / 2;
const padY = (INPUT - newH) / 2;
console.log(`scale=${scale} novo tamanho=${newW}x${newH} padX=${padX} padY=${padY}`);
// caixa prevista pelo modelo dentro da área 640x640
const boxInInput = [100, 200, 300, 400];
const back = [
(boxInInput[0] - padX) / scale,
(boxInInput[1] - padY) / scale,
(boxInInput[2] - padX) / scale,
(boxInInput[3] - padY) / scale,
];
console.log("Caixa na imagem original:", back.map(v => Math.round(v)));
Saída real
scale=0.5 novo tamanho=640x360 padX=0 padY=140
Caixa na imagem original: [ 200, 120, 600, 520 ]
Exemplo 10: Desenhando caixas com caracteres
Este exemplo desenha caixas em uma tela de caracteres 40x12, para que você veja onde os objetos estão sem usar nenhuma biblioteca gráfica.
// Desenhe as caixas sobre uma tela de caracteres para ver o resultado da detecção sem biblioteca gráfica
// A imagem original de 400x240 pixels é reduzida para uma grade de 40x12 células (cada célula com 10x20 pixels)
const COLS = 40, ROWS = 12, SX = 10, SY = 20;
const canvas = Array.from({ length: ROWS }, () => Array(COLS).fill("."));
const dets = [
{ label: "car", box: [40, 60, 180, 140] },
{ label: "person", box: [300, 50, 340, 170] },
];
for (const d of dets) {
const [x1, y1, x2, y2] = d.box;
const c1 = Math.floor(x1 / SX), c2 = Math.floor(x2 / SX);
const r1 = Math.floor(y1 / SY), r2 = Math.floor(y2 / SY);
for (let c = c1; c <= c2; c++) { canvas[r1][c] = "-"; canvas[r2][c] = "-"; }
for (let r = r1; r <= r2; r++) { canvas[r][c1] = "|"; canvas[r][c2] = "|"; }
canvas[r1][c1] = d.label[0].toUpperCase();
}
console.log(canvas.map(row => row.join("")).join("\n"));
Saída real
........................................
........................................
..............................P---|.....
....C-------------|...........|...|.....
....|.............|...........|...|.....
....|.............|...........|...|.....
....|.............|...........|...|.....
....|-------------|...........|...|.....
..............................|---|.....
........................................
........................................
........................................
Exemplo 11: Escrevendo o resultado como SVG
SVG é um formato de texto que pode ser gerado por código. Use-o para desenhar caixas e rótulos sobre uma imagem. Ele pode ser aberto em qualquer navegador.
// Crie um arquivo SVG que sobrepõe as caixas à imagem, para exibir ou salvar como resultado da detecção
const dets = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "person", score: 0.88, box: [300, 50, 340, 170] },
];
const lines = ['<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240">'];
lines.push('<rect width="400" height="240" fill="#1b1f2a"/>');
for (const d of dets) {
const [x1, y1, x2, y2] = d.box;
lines.push(`<rect x="${x1}" y="${y1}" width="${x2 - x1}" height="${y2 - y1}" fill="none" stroke="#ffb020" stroke-width="2"/>`);
lines.push(`<text x="${x1}" y="${y1 - 4}" font-size="12" fill="#ffb020">${d.label} ${d.score.toFixed(2)}</text>`);
}
lines.push("</svg>");
const svg = lines.join("\n");
console.log(svg);
console.log("Número de linhas:", lines.length, "tamanho:", svg.length, "caracteres");
Saída real
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240">
<rect width="400" height="240" fill="#1b1f2a"/>
<rect x="40" y="60" width="140" height="80" fill="none" stroke="#ffb020" stroke-width="2"/>
<text x="40" y="56" font-size="12" fill="#ffb020">car 0.94</text>
<rect x="300" y="50" width="40" height="120" fill="none" stroke="#ffb020" stroke-width="2"/>
<text x="300" y="46" font-size="12" fill="#ffb020">person 0.88</text>
</svg>
Número de linhas: 7 tamanho: 441 caracteres
Exemplo 12: Precisão e recall
Compare as previsões com a resposta correta (ground truth). TP são detecções corretas, FP são alarmes falsos e FN são objetos que ficaram sem detecção. A precisão é a parte das previsões que estão certas. O recall é a parte dos objetos reais que foram encontrados.
// Compare as previsões com a resposta certa: caixas com IoU >= 0.5 e mesma classe são TP, o resto é FP
// Respostas certas que não foram previstas são FN
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
const gts = [
{ label: "car", box: [40, 60, 180, 140] },
{ label: "person", box: [300, 50, 340, 170] },
{ label: "dog", box: [10, 150, 60, 200] },
];
const preds = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "person", score: 0.88, box: [305, 55, 345, 175] },
{ label: "car", score: 0.40, box: [200, 70, 260, 130] },
];
const used = new Array(gts.length).fill(false);
let tp = 0, fp = 0;
for (const p of [...preds].sort((a, b) => b.score - a.score)) {
let best = -1, bestIou = 0.5;
gts.forEach((g, i) => {
const v = iou(p.box, g.box);
if (!used[i] && g.label === p.label && v >= bestIou) { bestIou = v; best = i; }
});
if (best >= 0) { used[best] = true; tp++; } else fp++;
console.log(`${p.label.padEnd(7)} ${p.score.toFixed(2)} ${best >= 0 ? "TP" : "FP"}`);
}
const fn = gts.length - tp;
console.log(`TP=${tp} FP=${fp} FN=${fn}`);
console.log("precisão =", (tp / (tp + fp)).toFixed(2));
console.log("recall =", (tp / (tp + fn)).toFixed(2));
Saída real
car 0.94 TP
person 0.88 TP
car 0.40 FP
TP=2 FP=1 FN=1
precisão = 0.67
recall = 0.67
Exemplo 13: Average Precision (AP)
AP é a área sob a curva precisão-recall, com as previsões ordenadas pela confiança. Ele fornece um único número para comparar modelos.
// Average Precision (AP): resuma a curva precisão-recall em um único número
// Ordene as caixas pela confiança, depois calcule a precisão e o recall acumulados um a um
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
const gts = [
{ label: "car", box: [40, 60, 180, 140] },
{ label: "person", box: [300, 50, 340, 170] },
{ label: "dog", box: [10, 150, 60, 200] },
];
const preds = [
{ label: "car", score: 0.94, box: [40, 60, 180, 140] },
{ label: "person", score: 0.88, box: [305, 55, 345, 175] },
{ label: "car", score: 0.40, box: [200, 70, 260, 130] },
{ label: "dog", score: 0.30, box: [12, 152, 62, 202] },
];
const sorted = [...preds].sort((a, b) => b.score - a.score);
const used = new Array(gts.length).fill(false);
const flags = sorted.map(p => {
let best = -1, bestIou = 0.5;
gts.forEach((g, i) => {
const v = iou(p.box, g.box);
if (!used[i] && g.label === p.label && v >= bestIou) { bestIou = v; best = i; }
});
if (best >= 0) { used[best] = true; return 1; }
return 0;
});
let tp = 0, fp = 0;
const precision = [], recall = [];
flags.forEach(f => {
if (f) tp++; else fp++;
precision.push(tp / (tp + fp));
recall.push(tp / gts.length);
});
for (let i = precision.length - 2; i >= 0; i--) precision[i] = Math.max(precision[i], precision[i + 1]);
let ap = 0, prevRecall = 0;
recall.forEach((r, i) => { ap += (r - prevRecall) * precision[i]; prevRecall = r; });
sorted.forEach((p, i) => console.log(`${p.score.toFixed(2)} ${flags[i] ? "TP" : "FP"} recall=${recall[i].toFixed(2)} precisão=${precision[i].toFixed(2)}`));
console.log("AP =", ap.toFixed(3));
Saída real
0.94 TP recall=0.33 precisão=1.00
0.88 TP recall=0.67 precisão=1.00
0.40 FP recall=0.67 precisão=0.75
0.30 TP recall=1.00 precisão=0.75
AP = 0.917
Exemplo 14: Pipeline completo
Este exemplo junta as etapas: ler a saída bruta, filtrar pelo limite, aplicar NMS e mapear as caixas de volta para a imagem original.
// Exemplo combinado: saída bruta -> filtro por limite -> NMS -> converter coordenadas de volta para a imagem original
const sigmoid = x => 1 / (1 + Math.exp(-x));
const NAMES = ["person", "car", "dog"];
const STRIDE = 100, INPUT = 400;
const ORIG = { w: 1280, h: 720 };
const scale = Math.min(INPUT / ORIG.w, INPUT / ORIG.h);
const padX = (INPUT - ORIG.w * scale) / 2;
const padY = (INPUT - ORIG.h * scale) / 2;
const raw = [
{ row: 1, col: 1, tx: 0.1, ty: -0.2, tw: 0.2, th: 0.0, obj: 2.5, cls: [-1.0, 3.0, 0.2] },
{ row: 1, col: 2, tx: 0.0, ty: 0.1, tw: 0.1, th: 0.2, obj: 2.0, cls: [-0.5, 2.8, 0.0] },
{ row: 2, col: 0, tx: 0.3, ty: 0.0, tw: -0.4, th: -0.2, obj: 1.5, cls: [2.5, -1.0, 0.1] },
{ row: 3, col: 3, tx: -0.5, ty: 0.2, tw: 0.0, th: 0.1, obj: -2.0, cls: [0.0, 0.0, 3.0] },
];
const dets = raw.map(r => {
const cls = r.cls.map(sigmoid);
const best = cls.indexOf(Math.max(...cls));
const cx = (r.col + sigmoid(r.tx)) * STRIDE;
const cy = (r.row + sigmoid(r.ty)) * STRIDE;
const w = Math.exp(r.tw) * STRIDE;
const h = Math.exp(r.th) * STRIDE;
const score = sigmoid(r.obj) * cls[best];
return { label: NAMES[best], score, box: [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2] };
});
const kept = dets.filter(d => d.score >= 0.5);
console.log("Depois do filtro de limite 0.5:", kept.length, "caixas");
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
const final = [];
for (const d of [...kept].sort((a, b) => b.score - a.score)) {
if (final.every(f => f.label !== d.label || iou(f.box, d.box) < 0.5)) final.push(d);
}
console.log("Depois do NMS:", final.length, "caixas");
for (const d of final) {
const [x1, y1, x2, y2] = d.box.map((v, i) => (v - (i % 2 === 0 ? padX : padY)) / scale);
console.log(`${d.label.padEnd(7)} ${d.score.toFixed(2)} [${[x1, y1, x2, y2].map(v => Math.round(v)).join(", ")}]`);
}
Saída real
Depois do filtro de limite 0.5: 3 caixas
Depois do NMS: 3 caixas
car 0.88 [293, 24, 683, 344]
car 0.83 [623, 13, 977, 403]
person 0.76 [77, 389, 291, 651]
Exemplo 15: Contando objetos em um vídeo
Um vídeo é uma sequência de muitos quadros. Este exemplo conta as classes em cada quadro e depois usa a classe que mais aparece nos últimos três quadros para reduzir a oscilação.
// Um vídeo é uma sequência de imagens. Verifique cada quadro e conte as classes
// Olhar os valores de vários quadros vizinhos ajuda a reduzir a oscilação das previsões
const frames = [
["car", "car", "person"],
["car", "person", "person"],
["car", "car", "person"],
["car", "car"],
["person", "car", "car"],
];
frames.forEach((objs, i) => {
const counts = {};
for (const o of objs) counts[o] = (counts[o] || 0) + 1;
console.log(`Quadro ${i + 1}:`, JSON.stringify(counts));
});
// Use a classe que mais aparece em uma janela de 3 quadros
function majority(window) {
const c = {};
for (const x of window) c[x] = (c[x] || 0) + 1;
return Object.entries(c).sort((a, b) => b[1] - a[1])[0][0];
}
const labels = frames.map(f => f[0]);
const smoothed = labels.map((_, i) => majority(labels.slice(Math.max(0, i - 2), i + 1)));
console.log("Rótulo por quadro:", labels.join(" "));
console.log("Depois de olhar 3 quadros para trás:", smoothed.join(" "));
Saída real
Quadro 1: {"car":2,"person":1}
Quadro 2: {"car":1,"person":2}
Quadro 3: {"car":2,"person":1}
Quadro 4: {"car":2}
Quadro 5: {"person":1,"car":2}
Rótulo por quadro: car car car car person
Depois de olhar 3 quadros para trás: car car car car car
Próximos passos
Para executar um modelo de verdade, instale o Python 3.12, rode pip install ultralytics e carregue um modelo treinado, como YOLO('yolo11n.pt'). Forneça uma imagem para obter as caixas, as classes e as pontuações de confiança. A saída tem a mesma forma dos exemplos aqui, então a lógica de NMS e de coordenadas continua valendo.
Resumo
A detecção de objetos encontra caixas e classes de objetos em uma imagem. A etapa principal é decodificar a saída da grade, filtrar pela confiança, aplicar NMS e mapear as caixas de volta para a imagem original. A qualidade é medida com precisão, recall e AP. Cada saída deste artigo vem de um processo que foi realmente executado.