YOLOv5x 在 Horizon J6 上的端到端部署实践(下):板端推理与精度评估

上篇完成了模型从 PyTorch 到 HBM 的转换。本篇继续介绍板端 C++ 推理部署,包括 NV12 输入构造、UCP/DNN API 调用、YOLOv5 输出解析、交叉编译、板端运行以及 mAP 精度评估。


1. 板端推理整体流程

板端 C++ 推理可以拆成以下几步:

JPG 图片
  -> cv::imread 读取 BGR
  -> Letterbox Resize 到 640x640,padding=114
  -> BGR 转 YUV_I420,再转 NV12
  -> 拆成 Y 平面和 UV 平面
  -> 填入 hbDNNTensor
  -> 调用 hbDNNInferV2 执行 BPU 推理
  -> 按 tensor stride 解析输出
  -> YOLOv5 decode + NMS
  -> 映射回原图坐标
  -> 画框并保存结果

部署时最容易出问题的地方主要有两个:

  • 输入必须是模型期望的 NV12 双平面格式。
  • 输出不能按紧密内存布局读取,必须按 stride 访问。

2. NV12 输入格式

HBM 的输入通常会被拆成两个 tensor:

Tensor Shape 数据类型 说明
images_y (1, 640, 640, 1) U8 Y 亮度平面
images_uv (1, 320, 320, 2) U8 UV 交错色度平面

从 OpenCV 读取的 BGR 图片需要先做 letterbox,再转换为 NV12:

BGR
  -> Letterbox Resize
  -> cv::COLOR_BGR2YUV_I420
  -> 拆分 Y/U/V
  -> U/V 交错
  -> NV12

3. UCP/DNN API 调用流程

板端推理主流程可以简化为:

// 1. 加载模型
hbDNNPackedHandle_t packed_handle;
hbDNNInitializeFromFiles(&packed_handle, &model_path, 1);
hbDNNGetModelHandle(&dnn_handle, packed_handle, model_name_list[0]);

// 2. 准备输入输出 tensor
hbDNNGetInputTensorProperties(&input.properties, dnn_handle, i);
hbUCPMallocCached(&input.sysMem, input_mem_size, 0);
hbDNNGetOutputTensorProperties(&output.properties, dnn_handle, i);
hbUCPMallocCached(&output.sysMem, output_mem_size, 0);

// 3. 填充输入并清 cache
// letterbox + BGR->NV12 + copy to Y/UV tensor
hbUCPMemFlush(&input.sysMem, HB_SYS_MEM_CACHE_CLEAN);

// 4. 提交推理任务
hbDNNInferV2(&task_handle, output, input, dnn_handle);
hbUCPSubmitTask(task_handle, &sched_param);
hbUCPWaitTaskDone(task_handle, 0);

// 5. 读取输出前 invalid cache
hbUCPMemFlush(&output.sysMem, HB_SYS_MEM_CACHE_INVALIDATE);

// 6. 释放资源
hbUCPReleaseTask(task_handle);
hbUCPFree(&input.sysMem);
hbUCPFree(&output.sysMem);
hbDNNRelease(packed_handle);

实际工程中还需要处理错误码、动态 stride、内存大小计算和多输入多输出遍历。


4. 输出解析:必须按 stride 访问

YOLOv5 的输出 tensor 中存在 padding,不能用普通的连续数组方式读取。例如:

shape  = (1, 3, 80, 80, 85)
stride = (7372800, 2457600, 30720, 384, 4)

正确访问方式是使用 byte stride 计算偏移:

const uint8_t *base_ptr = static_cast<const uint8_t *>(output.sysMem.virAddr);

int64_t offset = anchor * stride[1]
               + row    * stride[2]
               + col    * stride[3]
               + k      * stride[4];

float value = *reinterpret_cast<const float *>(base_ptr + offset);

不要这样做:

// 错误:把输出当作紧密布局,会读到 padding
int base = ((anchor * grid_h + row) * grid_w + col) * 85;

这是板端 YOLO 后处理最常见的坑之一。


5. YOLOv5 解码与 NMS

YOLOv5 每个检测头对应一个 stride 和一组 anchors:

P3/8:   (10,13),  (16,30),  (33,23)
P4/16:  (30,61),  (62,45),  (59,119)
P5/32:  (116,90), (156,198), (373,326)

解码公式:

cx = (sigmoid(tx) * 2 - 0.5 + col) * stride
cy = (sigmoid(ty) * 2 - 0.5 + row) * stride
bw = (sigmoid(tw) * 2) ** 2 * anchor_w
bh = (sigmoid(th) * 2) ** 2 * anchor_h

score = sigmoid(obj) * sigmoid(cls)

后处理一般包含:

  1. 遍历三个检测头。
  2. 对 obj 和 class 做 sigmoid。
  3. 按置信度阈值过滤候选框。
  4. 将 letterbox 坐标映射回原图。
  5. 执行 NMS。
  6. 绘制检测框和类别标签。

6. 交叉编译

使用 SDK 自带的 aarch64 交叉编译器:

LINARO_GCC_ROOT="/arm-gnu-toolchain-12.2.rel1-x86_64-aarch64-none-linux-gnu"
export CC="${LINARO_GCC_ROOT}/bin/aarch64-none-linux-gnu-gcc"
export CXX="${LINARO_GCC_ROOT}/bin/aarch64-none-linux-gnu-g++"

cmake .
make -j$(nproc)

CMake 中需要链接 DNN、UCP、OpenCV 等运行依赖:

target_link_libraries(yolov5x_infer
    dnn hbucp gflags hlog fmt opencv_world
    bpu hbmem hbipcfhal alog jsoncpp cjson vdsp
    pthread rt dl)

如果共享库中的部分符号由板端系统运行时提供,可以加入:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wl,-unresolved-symbols=ignore-all")

7. 部署到板端运行

将可执行文件、HBM 模型和测试图片拷贝到板端:

scp build/yolov5x_infer root@<board_ip>:/map/zhonghua.xue/yolov5x/
scp model_output/yolov5x_640x640_nv12.hbm root@<board_ip>:/map/zhonghua.xue/yolov5x/
scp test.jpg root@<board_ip>:/map/zhonghua.xue/yolov5x/

板端执行:

cd /map/zhonghua.xue/yolov5x/

./yolov5x_infer \
  --model_file yolov5x_640x640_nv12.hbm \
  --image_file test.jpg \
  --output result.jpg \
  --conf_threshold 0.25 \
  --nms_threshold 0.45

如果结果框明显异常,优先检查三件事:

  1. 输入 NV12 是否正确。
  2. 是否重复做了 /255。
  3. 输出是否按 tensor stride 读取。

8. mAP 精度评估

部署完成后,可以对比浮点模型和量化模型在 COCO val2017 上的 mAP:

# 浮点 ONNX 模型
python3 stage5_evaluate.py origin 20
python3 stage5_evaluate.py origin

# 量化 BC 模型,PC 端可用 ONEDNN 后端
python3 stage5_evaluate.py quanti 20 --backend ONEDNN
python3 stage5_evaluate.py quanti --backend ONEDNN

# 两者对比
python3 stage5_evaluate.py both 100 --backend ONEDNN

示例结果:

Metric Float ONNX Quantized BC Diff
mAP 0.4913 0.4826 -0.0087
mAP_50 0.6424 0.6509 +0.0085
mAP_75 0.5241 0.5054 -0.0187
mAP_small 0.3575 0.3196 -0.0379
mAP_med 0.5280 0.5066 -0.0214
mAP_large 0.6710 0.6771 +0.0061

量化后 mAP 有轻微下降属于正常现象。若下降过大,优先排查校准数据覆盖度、预处理一致性、输入归一化和后处理 decode 是否一致。


9. 关键点速查

环节 要点
输入格式 板端 HBM 输入为 NV12 双平面
归一化 由编译配置中的 scale_value 完成
校准数据 使用 RGB、NCHW、float32、[0,1]
预处理 Stage 2、Stage 4、Stage 5 的 letterbox 逻辑必须一致
输出解析 必须按 tensor byte stride 访问
后处理 YOLOv5 decode、坐标映射、NMS 要和浮点侧保持一致

小结

下篇完成了从 HBM 模型到板端 C++ 推理的部署闭环。相比工具链编译,板端实现更容易踩到数据格式和内存布局问题。只要保证 NV12 输入正确、运行时归一化不重复、输出按 stride 解析,YOLOv5x 在 J6 上的端到端部署就能稳定跑通。

Logo

加入社区

更多推荐