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

浏览器中ONNX分割模型输出的后处理(掩码转换与尺寸调整)技术求助

浏览器中ONNX分割模型输出的后处理(掩码转换与尺寸调整)技术求助

嘿,刚好之前在浏览器端折腾过ONNX分割模型的后处理,给你分享下我当时的实现思路和代码片段,应该能帮你解决问题!

一、把模型输出转成可用掩码

首先得看你的模型是二分类还是多分类,两种情况的处理逻辑略有不同:

情况1:二分类分割模型(比如前景/背景分割)

这类模型的输出通常是[1, 1, maskH, maskW]形状的张量,每个像素是一个概率值(0到1之间)。我们可以设置一个阈值,把概率大于阈值的像素标记为前景,否则为背景:

// 输入:ONNX Runtime返回的输出张量
async function convertBinaryOutputToMask(outputTensor) {
  // 提取张量的形状和原始数据
  const [_, __, maskH, maskW] = outputTensor.dims;
  const outputData = outputTensor.data;
  
  // 初始化掩码数组,用Uint8Array存储更节省内存
  const maskData = new Uint8Array(maskH * maskW);
  
  // 遍历每个像素,根据阈值生成掩码
  const threshold = 0.5; // 可以根据你的模型调整这个值
  for (let i = 0; i < maskH * maskW; i++) {
    // 把前景设为255(方便后续可视化),背景设为0
    maskData[i] = outputData[i] > threshold ? 255 : 0;
  }
  
  return { maskData, maskH, maskW };
}

情况2:多分类分割模型

这类模型输出是[1, numClasses, maskH, maskW]形状的张量,每个像素对应多个类别的概率值。我们需要对每个像素取概率最大的类别作为该像素的分类结果:

async function convertMultiClassOutputToMask(outputTensor) {
  const [_, numClasses, maskH, maskW] = outputTensor.dims;
  const outputData = outputTensor.data;
  const maskData = new Uint8Array(maskH * maskW);
  
  // 遍历每个像素,找到概率最高的类别
  for (let y = 0; y < maskH; y++) {
    for (let x = 0; x < maskW; x++) {
      let maxProb = -Infinity;
      let targetClass = 0;
      
      // 遍历当前像素的所有类别概率
      for (let c = 0; c < numClasses; c++) {
        const idx = c * maskH * maskW + y * maskW + x;
        if (outputData[idx] > maxProb) {
          maxProb = outputData[idx];
          targetClass = c;
        }
      }
      
      // 把类别索引转成可视化的数值(这里给每个类别分配不同的亮度值)
      maskData[y * maskW + x] = targetClass * 50;
    }
  }
  
  return { maskData, maskH, maskW };
}

二、掩码的尺寸调整与可视化

通常模型输出的掩码尺寸会比输入图片小(比如输入是640x480,输出是320x240),这时候我们需要把掩码 resize 到和原图一致的尺寸,再叠加到原图上可视化。

我当时用的是OffscreenCanvas来做缩放,效率比普通Canvas高,不会阻塞主线程:

// 调整掩码尺寸到目标大小(比如原图的宽高)
function resizeMask(maskData, originalMaskW, originalMaskH, targetW, targetH) {
  // 创建原始尺寸的离屏Canvas,把掩码画上去
  const originalCanvas = new OffscreenCanvas(originalMaskW, originalMaskH);
  const originalCtx = originalCanvas.getContext('2d');
  const originalImageData = originalCtx.createImageData(originalMaskW, originalMaskH);
  
  // 把掩码数据填充到ImageData中
  for (let i = 0; i < maskData.length; i++) {
    // 单通道掩码,把R/G/B都设为掩码值,Alpha通道设为255
    originalImageData.data[i * 4] = maskData[i];
    originalImageData.data[i * 4 + 1] = maskData[i];
    originalImageData.data[i * 4 + 2] = maskData[i];
    originalImageData.data[i * 4 + 3] = 255;
  }
  originalCtx.putImageData(originalImageData, 0, 0);
  
  // 创建目标尺寸的离屏Canvas,进行缩放绘制
  const targetCanvas = new OffscreenCanvas(targetW, targetH);
  const targetCtx = targetCanvas.getContext('2d');
  targetCtx.drawImage(originalCanvas, 0, 0, targetW, targetH);
  
  // 提取缩放后的掩码数据
  const resizedImageData = targetCtx.getImageData(0, 0, targetW, targetH);
  const resizedMaskData = new Uint8Array(targetW * targetH);
  for (let i = 0; i < targetW * targetH; i++) {
    resizedMaskData[i] = resizedImageData.data[i * 4];
  }
  
  return resizedMaskData;
}

最后是可视化:把掩码叠加到原图上

你可以在页面上放一个Canvas,把原图和半透明的掩码画在一起:

// 输入:页面上的原图元素、掩码数据、掩码原始尺寸
async function renderSegmentationResult(originalImgEl, maskData, maskW, maskH) {
  const canvas = document.getElementById('seg-result-canvas');
  canvas.width = originalImgEl.width;
  canvas.height = originalImgEl.height;
  const ctx = canvas.getContext('2d');
  
  // 先画原图
  ctx.drawImage(originalImgEl, 0, 0);
  
  // 调整掩码到原图尺寸
  const resizedMask = resizeMask(maskData, maskW, maskH, canvas.width, canvas.height);
  
  // 创建掩码的ImageData,设置半透明效果
  const maskImageData = ctx.createImageData(canvas.width, canvas.height);
  for (let i = 0; i < resizedMask.length; i++) {
    if (resizedMask[i] > 0) {
      // 这里用红色半透明作为前景标记,你可以改成其他颜色
      maskImageData.data[i * 4] = 255; // R
      maskImageData.data[i * 4 + 1] = 0; // G
      maskImageData.data[i * 4 + 2] = 0; // B
      maskImageData.data[i * 4 + 3] = 80; // Alpha,值越小越透明
    } else {
      maskImageData.data[i * 4 + 3] = 0; // 背景全透明
    }
  }
  
  // 把掩码画到Canvas上,叠加在原图上方
  ctx.putImageData(maskImageData, 0, 0);
}

一些小提示

  • 阈值调整:二分类的阈值可以根据你的模型实际输出调整,比如如果模型输出的前景概率普遍偏低,可以把阈值设为0.3试试
  • 多分类颜色映射:如果是多分类,你可以给每个类别定义不同的RGB颜色,比如用一个数组存储[[255,0,0], [0,255,0], [0,0,255]]这样的颜色值,然后根据classIdx取对应的颜色
  • 性能优化:如果你的模型输出尺寸很大,尽量用TypedArray(比如Uint8Array)存储数据,比普通数组效率高很多

备注:内容来源于stack exchange,提问作者Krishna Kurdekar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 17:24:29