征程6|YOLOv5x 在 Horizon 征程6 上的端到端部署实践(下)

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

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

1. 板端推理整体流程

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

rust 复制代码
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:

rust 复制代码
BGR
  -> Letterbox Resize
  -> cv::COLOR_BGR2YUV_I420
  -> 拆分 Y/U/V
  -> U/V 交错
  -> NV12

3. UCP/DNN API 调用流程

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

scss 复制代码
// 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,不能用普通的连续数组方式读取。例如:

ini 复制代码
shape  = (1, 3, 80, 80, 85)
stride = (7372800, 2457600, 30720, 384, 4)

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

ini 复制代码
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);

不要这样做:

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

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

5. YOLOv5 解码与 NMS

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

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

解码公式:

ini 复制代码
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 交叉编译器:

ini 复制代码
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 等运行依赖:

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

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

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

7. 部署到板端运行

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

ruby 复制代码
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/

板端执行:

css 复制代码
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:

scss 复制代码
# 浮点 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 上的端到端部署就能稳定跑通。

相关推荐
speop1 小时前
llm-algo-leetcode |Task01
算法·leetcode·职场和发展
艾为电子2 小时前
【应用方案】电视沉浸式音频升级: 电视音频 awinic“芯片 + 算法” 一体化解决方案
算法·音视频
大熊背2 小时前
树莓派相机自动白平衡详解(二)
算法·白平衡·isppipeline
Scabbards_2 小时前
面试Leetcode - 算法合集
算法·leetcode·面试
chuan.bai2 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法
wuyk5552 小时前
7.AVL 树:第一个自平衡二叉搜索树
开发语言·stm32·单片机·算法
rannn_1112 小时前
【力扣hot100】二叉树专题+总结
java·算法·leetcode·二叉树
啊嘞嘞?2 小时前
力扣(岛屿数量)
算法·leetcode
zander2583 小时前
LeetCode 198. 打家劫舍
算法·leetcode·深度优先