前言:

在上篇博客中撰写了关于yolov5在RK3588上运行的流程, 在学习的过程中发现后处理很难,我一直不理解输出结果是如何存储的,它比较抽象,对于我来讲很难理解,经过一段时间的学习,有了一个比较好的理解思路。

1. 后处理的传参

在rknn_outputs_get 中获得了NPU推理后的处理结果,这个结果有三个输出结果outputs[i].buf,作为后处理的输入部分,对应模型三个尺度的输出张量。

rknn_outputs_get(ctx, outputs_num, outputs, NULL);
post_process((int8_t *)outputs[0].buf, (int8_t *)outputs[1].buf, (int8_t *)outputs[2].buf,....)

这里的outputs是三个检测头的输出特征图

  • YOLOv5采用多尺度检测,三个输出头对应不同下采样步长(这里输出有三个不同类型是因为有不同的检测目标)
输出头步长网格大小锚框尺寸检测目标
output0880×80[10,13], [16,30], [33,23]小目标
output11640×40[30,61], [62,45], [59,119]中目标
output23220×20[116,90], [156,198], [373,326]大目标

下面我们对于这个输出的结果进行理解,有助于理解后续后处理code的编写

2. 数据格式

  • 数据类型:int8_t(量化后的 8 位整数)
  • 内存布局:NCHW 或 NHWC(取决于模型导出时的配置,此处代码按 NCHW 处理)
  • 每个输出是一个 4 维张量,形状为 [batch(图片数量), channels(通道), grid_h(特征图高度), grid_w(特征图宽度)](batch=1),例如:
    • output0:[1, 255, 80, 80](stride=8)
    • output1:[1, 255, 40, 40](stride=16)
    • output2:[1, 255, 20, 20](stride=32)

其中 255 = 3 × (80 + 5):

  • 3:每个网格预测 3 个锚框(anchor0/1/2)
  • 80:COCO 数据集的 80 个类别概率
  • 5:x, y, w, h, objectness(框坐标和置信度)

3. 数据内容

每个网格的每个锚框对应一组长度为 85 的数据,排列顺序通常是:

[x, y, w, h, obj_conf(是否存在目标), cls0_prob(第1个类), cls1_prob(第2个类), ..., cls79_prob(第80个类)]

在这里你还是不知道如何存储的话,听我下面的讲解

4. 数据的立体讲解-----最重要理解方式

想象一个 三维立方体,三个维度分别是:

  • 通道 C:0 ~ 254(共 255 层)
  • 高度 H:0 ~ 79(共 80 行)
  • 宽度 W:0 ~ 79(共 80 列)

也就是说,我们对立方体横向砍一刀,得到一个横截面,它的宽和高是H和W,立方体的高是对应着通道C, 我们可以将高度和C一一对应

  • 它一共有255层,对应255个通道数

  • **255个通道数如何排列?**如下图所示

    [
      x,
      y,
      w,
      h,
      obj_conf,
      cls0_prob,
      cls1_prob,
      ...,
      cls79_prob
    ]
    

    上面总计85个
    这个立方体有85*3 = 255, 为什么有要× 3 ?有3个锚框

    • 那么锚框0,对应通道0-84
    • 锚框1,对应通道85-169
    • 锚框2,对应通道170-254

    在这里插入图片描述
    在这里插入图片描述

5. 如何理解内存中的存储?

以output0:[1, 255, 80, 80](stride=8)这个为例子,

特征图的宽和高为80×80, 则x、y、w、h、c 将会分别占用6400的存储单元(x占6400个,y占6400个,w占6400个…)

  • 从立方体的从上往下看,先对应的是x,后对应的是y,w等,那么存储的时候,先在内存中存放6400个x, 后存6400个y, 再就是6400个w,依次存完。

  • 由于有三个锚框先存完「第 1 个锚框」的全部数据(X/Y/W/H/C/80 个类),再接着存「第 2 个锚框」的全部数据,最后存「第 3 个锚框」的全部数据。

  • 单个锚框存储的总单元:

    • 基础 5 参数:X(6400) + Y(6400) + W(6400) + H(6400) + C(6400) = 5×6400
    • 80 个类别:80×6400
    • 单个锚框总长度:(5+80)×6400 = 85×6400 =544000
  • 3个锚框:(5 + 80 )* 3 * 6400 = 1,632,000

内存地址低 → 高
┌──────────────────────┐
│  通道 0 (x_a0)       │  6400 个 int8
│  通道 1 (y_a0)       │  6400 个 int8
│  通道 2 (w_a0)       │  6400 个 int8
│  通道 3 (h_a0)       │  6400 个 int8
│  通道 4 (obj_a0)     │  6400 个 int8
│  通道 5 (cls0_a0)    │  6400 个 int8
│  ...                 │
│  通道 84 (cls79_a0)  │  6400 个 int8
├──────────────────────┤
│  通道 85 (x_a1)      │  6400 个 int8
│  ...                 │
│  通道 169 (cls79_a1) │  6400 个 int8
├──────────────────────┤
│  通道 170 (x_a2)     │  6400 个 int8
│  ...                 │
│  通道 254 (cls79_a2) │  6400 个 int8
└──────────────────────┘
总大小 = 255 × 6400 = 1,632,000 个 int8

总结

相对来说比较清晰了吧?后面再告诉如何写后处理部分的code,如何找到对应的数据

更多推荐