You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解读React应用中YOLOv8 Web模型的输出张量?

YOLOv8 TFJS模型输出解析与边界框提取方案

首先明确你拿到的1x84x8400张量结构:

  • 1:批量大小(单张图片输入)
  • 84:每个检测框的参数,包含4个边界框坐标(中心x、中心y、宽、高) + 1个框置信度 + 80个类别概率(对应COCO数据集的80类)
  • 8400:模型生成的所有候选检测框数量

以下是完整的解析与提取流程,直接替换你的代码即可:

1. 修正输入预处理(关键)

YOLOv8要求输入像素值归一化到[0,1],且无需多余的维度转置,修正后的预处理代码:

const image = document.getElementsByClassName('catImage')[0];
// 转float32并归一化到0-1
const tfimg = tf.browser.fromPixels(image).toFloat().div(tf.scalar(255));
// 调整尺寸到模型要求的640x640
const tfimg_res = tf.image.resizeBilinear(tfimg, [640, 640]);
// 添加batch维度,得到[1, 640, 640, 3]格式的输入
const expandedimg = tfimg_res.expandDims(0);

2. 解析模型输出并转换坐标格式

将输出张量转成按检测框排列的结构,再把YOLO的中心坐标格式转成常用的左上角+右下角坐标,同时缩放回原图尺寸:

const predictions = await model.executeAsync(expandedimg);
// 把[1,84,8400]转成[1,8400,84],方便按检测框遍历
const reshaped = predictions[0].transpose([0, 2, 1]);
const boxesData = await reshaped.array();
const batchData = boxesData[0];

// 获取原图尺寸和缩放比例
const originalWidth = image.width;
const originalHeight = image.height;
const scaleX = originalWidth / 640;
const scaleY = originalHeight / 640;

// 处理每个候选框
const processedBoxes = batchData.map(box => {
  // 提取YOLO格式的坐标参数
  const xCenter = box[0];
  const yCenter = box[1];
  const width = box[2];
  const height = box[3];
  
  // 转成左上角(x1,y1)和右下角(x2,y2)坐标(基于640x640输入图)
  let x1 = xCenter - width / 2;
  let y1 = yCenter - height / 2;
  let x2 = xCenter + width / 2;
  let y2 = yCenter + height / 2;
  
  // 限制坐标在图像范围内,避免越界
  x1 = Math.max(0, Math.min(640, x1));
  y1 = Math.max(0, Math.min(640, y1));
  x2 = Math.max(0, Math.min(640, x2));
  y2 = Math.max(0, Math.min(640, y2));
  
  // 缩放回原图尺寸
  x1 *= scaleX;
  y1 *= scaleY;
  x2 *= scaleX;
  y2 *= scaleY;
  
  // 提取置信度和类别信息
  const boxConfidence = box[4];
  const classScores = box.slice(5);
  const classIndex = classScores.indexOf(Math.max(...classScores));
  const classScore = classScores[classIndex];
  // 最终置信度=框置信度×类别概率
  const finalConfidence = boxConfidence * classScore;
  
  return { x1, y1, x2, y2, confidence: finalConfidence, classIndex, classScore };
});

3. 非极大值抑制(NMS)过滤重叠框

YOLO会生成大量候选框,用NMS去除重叠度高、置信度低的框:

// 计算两个框的交并比(IOU)
function calculateIOU(boxA, boxB) {
  const x1 = Math.max(boxA.x1, boxB.x1);
  const y1 = Math.max(boxA.y1, boxB.y1);
  const x2 = Math.min(boxA.x2, boxB.x2);
  const y2 = Math.min(boxA.y2, boxB.y2);
  
  const intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
  const boxAArea = (boxA.x2 - boxA.x1) * (boxA.y2 - boxA.y1);
  const boxBArea = (boxB.x2 - boxB.x1) * (boxB.y2 - boxB.y1);
  
  return intersectionArea / (boxAArea + boxBArea - intersectionArea);
}

// 执行NMS
function nonMaxSuppression(boxes, iouThreshold = 0.5, confidenceThreshold = 0.25) {
  // 先过滤低置信度框
  const filteredBoxes = boxes.filter(box => box.confidence >= confidenceThreshold);
  // 按置信度从高到低排序
  filteredBoxes.sort((a, b) => b.confidence - a.confidence);
  
  const result = [];
  while (filteredBoxes.length > 0) {
    const currentBox = filteredBoxes.shift();
    result.push(currentBox);
    // 移除与当前框IOU超过阈值的框
    filteredBoxes.splice(0, filteredBoxes.length, ...filteredBoxes.filter(box => calculateIOU(currentBox, box) < iouThreshold));
  }
  return result;
}

// 得到最终过滤后的检测框
const finalBoxes = nonMaxSuppression(processedBoxes);

4. 映射类别名称

将类别索引转换为COCO数据集对应的类别名称:

// COCO 80类名称数组(顺序与YOLOv8输出完全一致)
const cocoClasses = [
  'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
  'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
  'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
  'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
  'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
  'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
  'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
  'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
  'hair drier', 'toothbrush'
];

// 给结果添加类别名称
const resultWithNames = finalBoxes.map(box => ({
  ...box,
  className: cocoClasses[box.classIndex]
}));

console.log('最终检测结果:', resultWithNames);

5. 完整整合代码

记得在最后清理张量,避免内存泄漏:

const imagePrediction = async () => {
  const model = await loadGraphModel('/yolon-model/model.json');
  const image = document.getElementsByClassName('catImage')[0];
  
  // 预处理
  const tfimg = tf.browser.fromPixels(image).toFloat().div(tf.scalar(255));
  const tfimg_res = tf.image.resizeBilinear(tfimg, [640, 640]);
  const expandedimg = tfimg_res.expandDims(0);
  
  // 预测
  const predictions = await model.executeAsync(expandedimg);
  const reshaped = predictions[0].transpose([0, 2, 1]);
  const boxesData = await reshaped.array();
  const batchData = boxesData[0];
  
  // 尺寸转换
  const originalWidth = image.width;
  const originalHeight = image.height;
  const scaleX = originalWidth / 640;
  const scaleY = originalHeight / 640;
  
  const processedBoxes = batchData.map(box => {
    const xCenter = box[0];
    const yCenter = box[1];
    const width = box[2];
    const height = box[3];
    
    let x1 = xCenter - width / 2;
    let y1 = yCenter - height / 2;
    let x2 = xCenter + width / 2;
    let y2 = yCenter + height / 2;
    
    x1 = Math.max(0, Math.min(640, x1));
    y1 = Math.max(0, Math.min(640, y1));
    x2 = Math.max(0, Math.min(640, x2));
    y2 = Math.max(0, Math.min(640, y2));
    
    x1 *= scaleX;
    y1 *= scaleY;
    x2 *= scaleX;
    y2 *= scaleY;
    
    const boxConfidence = box[4];
    const classScores = box.slice(5);
    const classIndex = classScores.indexOf(Math.max(...classScores));
    const classScore = classScores[classIndex];
    const finalConfidence = boxConfidence * classScore;
    
    return { x1, y1, x2, y2, confidence: finalConfidence, classIndex, classScore };
  });
  
  // NMS相关函数
  const calculateIOU = (boxA, boxB) => {
    const x1 = Math.max(boxA.x1, boxB.x1);
    const y1 = Math.max(boxA.y1, boxB.y1);
    const x2 = Math.min(boxA.x2, boxB.x2);
    const y2 = Math.min(boxA.y2, boxB.y2);
    
    const intersectionArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
    const boxAArea = (boxA.x2 - boxA.x1) * (boxA.y2 - boxA.y1);
    const boxBArea = (boxB.x2 - boxB.x1) * (boxB.y2 - boxB.y1);
    
    return intersectionArea / (boxAArea + boxBArea - intersectionArea);
  };
  
  const nonMaxSuppression = (boxes, iouThreshold = 0.5, confidenceThreshold = 0.25) => {
    const filteredBoxes = boxes.filter(box => box.confidence >= confidenceThreshold);
    filteredBoxes.sort((a, b) => b.confidence - a.confidence);
    
    const result = [];
    while (filteredBoxes.length > 0) {
      const currentBox = filteredBoxes.shift();
      result.push(currentBox);
      filteredBoxes.splice(0, filteredBoxes.length, ...filteredBoxes.filter(box => calculateIOU(currentBox, box) < iouThreshold));
    }
    return result;
  };
  
  const finalBoxes = nonMaxSuppression(processedBoxes);
  
  // 类别映射
  const cocoClasses = [
    'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
    'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow',
    'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee',
    'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard',
    'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
    'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch',
    'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone',
    'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear',
    'hair drier', 'toothbrush'
  ];
  
  const resultWithNames = finalBoxes.map(box => ({
    ...box,
    className: cocoClasses[box.classIndex]
  }));
  
  console.log('最终检测结果:', resultWithNames);
  
  // 清理张量
  tf.dispose([tfimg, tfimg_res, expandedimg, predictions, reshaped]);
};

内容的提问来源于stack exchange,提问作者Marcus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 19:24:57