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

如何解读Hololens2 Unity项目中ONNX目标检测模型的输出

处理Azure Custom Vision导出的ONNX目标检测模型输出(Hololens 2 + Unity)

一、明确输出张量结构

你的模型输出维度(n:1, h:13, w:13, c:30)对应:

  • n=1:批量大小,单张输入图像可忽略该维度
  • h=13/w=13:输入图像被划分为13×13的网格,每个网格负责检测对应区域的目标
  • c=30:每个网格的输出通道,对应3个预设锚框,每个锚框包含5个边界框参数(x,y,w,h,置信度) + 5个类别概率(3*(5+5)=30),类别数量和你在Azure Custom Vision中训练的类别数一致

二、解析输出数据的步骤

1. 准备基础参数

提前确认以下信息:

  • Hololens相机捕获的纹理原始宽高(如1280×720)
  • 模型训练时的预设锚框尺寸(可在导出模型的zip包中anchors.txt文件查看)
  • 置信度阈值(如0.5,过滤低置信度结果)
  • 类别置信度阈值(如0.3,过滤类别概率低的结果)
  • NMS(非极大值抑制)阈值(如0.5,去除重复检测框)

2. 遍历网格与锚框,解析检测结果

将Barracuda输出的数组转换为可识别的检测结果:

// 输入图像尺寸,替换为你的实际纹理尺寸
int inputWidth = 1280;
int inputHeight = 720;
// 替换为模型对应的锚框尺寸
float[][] anchors = new float[][] 
{
    new float[] {10f, 13f},
    new float[] {16f, 30f},
    new float[] {33f, 23f}
};
float confThreshold = 0.5f;
float classThreshold = 0.3f;

List<DetectionResult> rawResults = new List<DetectionResult>();

// 遍历13×13网格
for (int gridY = 0; gridY < 13; gridY++)
{
    for (int gridX = 0; gridX < 13; gridX++)
    {
        // 遍历3个锚框
        for (int anchorIdx = 0; anchorIdx < 3; anchorIdx++)
        {
            // 计算当前锚框在输出数组中的起始索引
            int channelStart = anchorIdx * 10;
            int dataIndex = gridY * 13 * 30 + gridX * 30 + channelStart;

            // 过滤低置信度锚框
            float confidence = Sigmoid(outputData[dataIndex + 4]);
            if (confidence < confThreshold)
                continue;

            // 解析边界框坐标,转换为图像绝对坐标
            float xOffset = Sigmoid(outputData[dataIndex]);
            float yOffset = Sigmoid(outputData[dataIndex + 1]);
            float boxWidth = Mathf.Exp(outputData[dataIndex + 2]) * anchors[anchorIdx][0];
            float boxHeight = Mathf.Exp(outputData[dataIndex + 3]) * anchors[anchorIdx][1];

            float gridSizeX = inputWidth / 13f;
            float gridSizeY = inputHeight / 13f;
            float centerX = (gridX + xOffset) * gridSizeX;
            float centerY = (gridY + yOffset) * gridSizeY;
            float left = centerX - boxWidth / 2;
            float top = centerY - boxHeight / 2;
            float right = centerX + boxWidth / 2;
            float bottom = centerY + boxHeight / 2;

            // 解析最高概率的类别
            float maxClassProb = 0;
            int targetClassIdx = -1;
            for (int c = 0; c < 5; c++)
            {
                float prob = Sigmoid(outputData[dataIndex + 5 + c]);
                if (prob > maxClassProb)
                {
                    maxClassProb = prob;
                    targetClassIdx = c;
                }
            }

            // 过滤低类别置信度结果
            float finalConf = confidence * maxClassProb;
            if (finalConf < classThreshold)
                continue;

            rawResults.Add(new DetectionResult()
            {
                Left = left,
                Top = top,
                Right = right,
                Bottom = bottom,
                ClassIndex = targetClassIdx,
                Confidence = finalConf
            });
        }
    }
}

// Sigmoid激活函数,模型输出的原始值需转换为0-1区间的概率
float Sigmoid(float x)
{
    return 1f / (1f + Mathf.Exp(-x));
}

// 检测结果数据类
public class DetectionResult
{
    public float Left;
    public float Top;
    public float Right;
    public float Bottom;
    public int ClassIndex;
    public float Confidence;
}

3. 非极大值抑制(NMS)去除重复框

多个锚框可能检测同一个目标,用NMS保留最准确的结果:

// 按置信度降序排序
rawResults.Sort((a, b) => b.Confidence.CompareTo(a.Confidence));

List<DetectionResult> finalResults = new List<DetectionResult>();
bool[] isSuppressed = new bool[rawResults.Count];

for (int i = 0; i < rawResults.Count; i++)
{
    if (isSuppressed[i])
        continue;

    finalResults.Add(rawResults[i]);

    for (int j = i + 1; j < rawResults.Count; j++)
    {
        if (isSuppressed[j])
            continue;

        float iou = CalculateIOU(rawResults[i], rawResults[j]);
        if (iou > 0.5f)
        {
            isSuppressed[j] = true;
        }
    }
}

// 计算交并比(IOU)
float CalculateIOU(DetectionResult a, DetectionResult b)
{
    float intersectLeft = Mathf.Max(a.Left, b.Left);
    float intersectTop = Mathf.Max(a.Top, b.Top);
    float intersectRight = Mathf.Min(a.Right, b.Right);
    float intersectBottom = Mathf.Min(a.Bottom, b.Bottom);

    if (intersectRight < intersectLeft || intersectBottom < intersectTop)
        return 0;

    float intersectArea = (intersectRight - intersectLeft) * (intersectBottom - intersectTop);
    float areaA = (a.Right - a.Left) * (a.Bottom - a.Top);
    float areaB = (b.Right - b.Left) * (b.Bottom - b.Top);

    return intersectArea / (areaA + areaB - intersectArea);
}

三、判断是否存在目标

处理完成后,若finalResults列表不为空,则说明输入纹理中包含自定义标记对象。你可以遍历该列表,获取目标的位置、类别和置信度,进而在Hololens中实现3D边界框可视化等操作。

注意事项

  • 锚框尺寸必须与Azure Custom Vision训练时的配置一致,否则边界框计算会出错
  • 输入图像尺寸需与模型训练时的输入尺寸匹配,若不一致需先缩放纹理
  • 若模型输出的置信度、类别概率已经是0-1区间的值,可跳过Sigmoid转换

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 10:44:50