【YOLOv8pose部署至RDK X5】模型训练→转换bin→Sunrise 5部署

已在GitHub开源与本博客同步的YOLOv8_RDKX5_object_pose项目,地址:https://github.com/A7bert777/YOLOv8_RDKX5_object_pose

详细使用教程,可参考README.md或参考本博客第八章 模型部署

注:本文是以sunrise5 SoC进行示例,旭日其他系列SoC的部署流程也基本一致,如需帮助,可通过Github仓库的 README.md 沟通。

文章目录

一、项目回顾

博主之前主要使用瑞芯微、昇腾系列的SoC及对应生态,现在逐渐转向地平线/地瓜系列,博主本人使用的是RDK X5开发套件,如下图所示,SoC为sunrise5,但发现CSDN上目前没有什么比较详细的免费文章与开源项目供大家入手,因此自己尝试进行完整流程的部署,遂以此文分享,供大家一起学习。

博主之前有写过在华为Ascend、瑞芯微RK系列SoC上的YOLOv8目标检测&图像分割、YOLOv10目标检测、MoblieNetv2图像分类的模型训练、转换、部署文章,感兴趣的小伙伴可以了解下:
【YOLOv8seg部署至RDK X5】模型训练→转换bin→Sunrise 5部署
【YOLOv8部署至RDK X5】模型训练→转换bin→Sunrise 5部署
【YOLOv8部署至Ascend 310B】模型训练→转换om→310B部署
【YOLO11-obb部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLO11部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLOv10部署RK3588】模型训练→转换rknn→部署流程
【YOLOv8-obb部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLOv8-pose部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLOv8seg部署RK3588】模型训练→转换rknn→部署全流程
【YOLOv8部署至RK3588】模型训练→转换rknn→部署全流程
【YOLOv7部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLOv6部署至RK3588】模型训练→转换RKNN→开发板部署
【YOLOv5部署至RK3588】模型训练→转换RKNN→开发板部署
【MobileNetv2图像分类部署至RK3588】模型训练→转换rknn→部署流程
【ResNet50图像分类部署至RK3588】模型训练→转换RKNN→开发板部署
YOLOv8n部署RK3588开发板全流程(pt→onnx→rknn模型转换、板端后处理检测)

二、文件梳理

之前博主发布过YOLOv8转RKNN模型并在开发板上部署的流程,现在尝试在地瓜的RDK X5 开发板上,使用旭日sunrise5进行YOLOv8实例分割模型的部署。

OK,进入正题,模型转换需要以下工具:

第一个文件:github上ultralytics的yolov8项目

第二个文件:github上地瓜的rdk_model_zoo仓库

第三个文件:博主个人Github仓库:YOLOv8_RDKX5_object_pose

三、模型训练

YOLOv8的模型训练环境配置、训练步骤,网上的很多相关教程很多,基础不多叙述,大家可以直接参考其他文章

这是博主的train.py:

python 复制代码
from ultralytics import YOLO

# 加载模型
model = YOLO("yolov8-pose.yaml")  # 从头开始构建新模型
#model = YOLO("yolov8n.pt")  # 加载预训练模型(推荐用于训练)

# Use the model 
# 以下设置最好不要改动!!!可能会出现关键点漂移的问题
results = model.train(
    data="knob-pose.yaml",
    epochs=300,
    batch=4,
    augment=False,  # 关闭所有数据增强
    degrees=0,      # 旋转角度=0
    translate=0,    # 平移=0
    scale=0,        # 缩放=0
    shear=0,        # 剪切=0
    flipud=0,       # 上下翻转概率=0
    fliplr=0        # 左右翻转概率=0
)

训练完成后,在当前路径下的runs/pose下生成我们的best.pt,我将其重命名为yolov8npose_knob.pt,博主比较喜欢实用ReLU激活函数,因此netron打开模型后如下所示:

将best.pt重命名为yolov8npose_knob.pt,如下所示:

四、PT转ONNX

先在项目下创建转换脚本export_monkey_patch.py,内容如下所示,直接复制:

python 复制代码
#!/user/bin/env python

# Copyright (c) 2025, WuChao D-Robotics.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# 注意: 此程序在Ultralytics模型的训练环境中运行
# Attention: This program runs on Ultralytics training Environment.

import argparse

from ultralytics import YOLO
from ultralytics.nn.modules.head import Detect, v10Detect, Segment, OBB, Pose, Classify
# from ultralytics.nn.modules.block import Attention, AAttn

import torch
import types
import os

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--pt', type=str, default='./yolo11n.pt', help='path to *.pt model.')
    parser.add_argument('--optse', type=int, default=11, help='opset version.')
    opt = parser.parse_args()

    # Init Ultralytics YOLO Model
    m = YOLO(opt.pt)

    # Replace some efficient modules
    modelZooOptimizer(m.model.model)

    # Export to ONNX
    m.export(format='onnx', simplify=False, opset=11)


def modelZooOptimizer(model):  # Monkey Patch
    for name, child in model.named_children():
        # print(name)
        if type(child) == Classify:
            child.forward = types.MethodType(Classify_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced Classify_forward in {name}" + "\033[0m")
        elif type(child) == Detect:
            child.forward = types.MethodType(Detect_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced Detect_forward in {name}" + "\033[0m")
        elif type(child) == v10Detect:
            child.forward = types.MethodType(v10Detect_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced v10Detect_forward in {name}" + "\033[0m")
        elif type(child) == Segment:
            child.forward = types.MethodType(Segment_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced Segment_forward in {name}" + "\033[0m")
        elif type(child) == Pose:
            child.forward = types.MethodType(Pose_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced Pose_forward in {name}" + "\033[0m")
        elif type(child) == OBB:
            child.forward = types.MethodType(OBB_forward, child)
            print("\033[1;31m" + f"[Cauchy] Replaced OBB_forward in {name}" + "\033[0m")
        # elif type(child) == AAttn:
        #     child.forward = types.MethodType(AAttn_forward, child)
        #     print("\033[1;31m" + f"[Cauchy] Replaced AAttn_forward in {name}" + "\033[0m")
        # elif type(child) == Attention:
        #     child.forward = types.MethodType(Attention_forward, child)
        #     print("\033[1;31m" + f"[Cauchy] Replaced Attention_forward in {name}" + "\033[0m")
        modelZooOptimizer(child)



def Attention_forward(self, x): 
    # Effieicient for Bayes-e BPU
    B, C, H, W = x.shape
    N = H * W
    qkv = self.qkv(x)
    q, k, v = qkv.view(B, self.num_heads, self.key_dim * 2 + self.head_dim, N).split([self.key_dim, self.key_dim, self.head_dim], dim=2)
    attn = (q.transpose(-2, -1) @ k) * self.scale
    attn = attn.permute(0, 3, 1, 2).contiguous()  # CHW2HWC like
    max_attn = attn.max(dim=1, keepdim=True).values 
    exp_attn = torch.exp(attn - max_attn)
    sum_attn = exp_attn.sum(dim=1, keepdim=True)
    attn = exp_attn / sum_attn
    attn = attn.permute(0, 2, 3, 1).contiguous()  # HWC2CHW like
    x = (v @ attn.transpose(-2, -1)).view(B, C, H, W) + self.pe(v.reshape(B, C, H, W))
    x = self.proj(x)
    return x

def AAttn_forward(self, x):
    # Effieicient for Bayes-e BPU
    B, C, H, W = x.shape
    N = H * W
    qkv = self.qkv(x).flatten(2).transpose(1, 2)
    if self.area > 1:
        qkv = qkv.reshape(B * self.area, N // self.area, C * 3)
        B, N, _ = qkv.shape
    q, k, v = (qkv.view(B, N, self.num_heads, self.head_dim * 3).permute(0, 2, 3, 1).split([self.head_dim, self.head_dim, self.head_dim], dim=2))
    attn = (q.transpose(-2, -1) @ k) * (self.head_dim**-0.5)
    attn = attn.permute(0, 3, 1, 2).contiguous()  # CHW2HWC like
    max_attn = attn.max(dim=1, keepdim=True).values 
    exp_attn = torch.exp(attn - max_attn)
    sum_attn = exp_attn.sum(dim=1, keepdim=True)
    attn = exp_attn / sum_attn
    attn = attn.permute(0, 2, 3, 1).contiguous()  # HWC2CHW like
    x = v @ attn.transpose(-2, -1)
    x = x.permute(0, 3, 1, 2)
    v = v.permute(0, 3, 1, 2)
    if self.area > 1:
        x = x.reshape(B // self.area, N * self.area, C)
        v = v.reshape(B // self.area, N * self.area, C)
        B, N, _ = x.shape
    x = x.reshape(B, H, W, C).permute(0, 3, 1, 2).contiguous()
    v = v.reshape(B, H, W, C).permute(0, 3, 1, 2).contiguous()
    x = x + self.pe(v)
    return self.proj(x)

def Classify_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    x = torch.cat(x, 1) if isinstance(x, list) else x
    return self.linear(self.drop(self.pool(self.conv(x)).flatten(1)))

def Detect_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    result = []
    for i in range(self.nl):
        result.append(self.cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())  # cls
        result.append(self.cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())  # bbox
    return result
def v10Detect_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    result = []
    for i in range(self.nl):
        result.append(self.one2one_cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())  # cls
        result.append(self.one2one_cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())  # bbox
    return result

def Segment_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    result = []
    for i in range(self.nl):
        result.append(self.cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())  # cls
        result.append(self.cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())  # bbox
        result.append(self.cv4[i](x[i]).permute(0, 2, 3, 1).contiguous())  # proto weights
    result.append(self.proto(x[0]).permute(0, 2, 3, 1).contiguous())       # proto mask
    return result

def Pose_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    result = []
    for i in range(self.nl):
        result.append(self.cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())  # cls
        result.append(self.cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())  # bbox
        result.append(self.cv4[i](x[i]).permute(0, 2, 3, 1).contiguous())  # kpts
    return result

def OBB_forward(self, x):
    # Effieicient for Bernoulli2, Bayes, Bayes-e, Nash-{e/m/p} BPU
    # TODO: Test and PostProcess Code in Model Zoo.
    result = []
    for i in range(self.nl):
        result.append(self.cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())  # cls
        result.append(self.cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())  # bbox
        result.append(self.cv4[i](x[i]).permute(0, 2, 3, 1).contiguous())  # theta logits
    return result


if __name__ == '__main__':
    main()

然后将yolov8npose_knob.pt也复制到ultralytics同级目录下,这样pt模型和转换脚本在同一目录下,如下所示:

执行命令,注意模型名改成你自己的:

bash 复制代码
python export_monkey_patch.py --pt yolov8npose_knob.pt

结果如下所示:

此时可以看到,在pt模型同路径下生成了对应的onnx模型:

可以看到,转出的onnx模型已经被改成9个输出tensor了,用netron打开如下所示:

五、ONNX转bin-Dokcer容器配置

--------------------------------这一步骤全部在PC端的虚拟机中完成--------------------------------

①:docker镜像文件下载:

bash 复制代码
wget -c ftp://x5ftp@vrftp.horizon.ai/OpenExplorer/v1.2.8_release/docker_openexplorer_ubuntu_20_x5_cpu_v1.2.8.tar.gz --ftp-password=x5ftp@123$%

如下所示:

②:加载docke镜像:

bash 复制代码
sudo docker load -i docker_openexplorer_ubuntu_20_x5_cpu_v1.2.8.tar.gz

如下所示:

③:查看确定镜像已加载:

bash 复制代码
docker images

六、ONNX转bin-OE工具链配置

--------------------------------这一步骤同样全部在PC端的虚拟机中完成--------------------------------

①:下载地瓜所需的OE工具链

bash 复制代码
wget -c ftp://x5ftp@vrftp.horizon.ai/OpenExplorer/v1.2.8_release/horizon_x5_open_explorer_v1.2.8-py310_20240926.tar.gz --ftp-password=x5ftp@123$%

如下所示:

下载完成后解压,文件夹内容如下所示,其中run_docker.sh是我们的启动容器的脚本:

②:打开run_docker.sh,在version参数后加-py310,如下所示:

修改后完整的run_docker.sh如下所示:

bash 复制代码
#!/bin/bash

dataset_path=$1
run_type=$2
version=v1.2.8-py310
container_name=$(whoami)_OE_v1.2.8
host_name=$(echo "1.2.8" |awk -F "." '{ print $1"-"$2"-"$3 }')

if [ -z "$dataset_path" ];then
  echo "Please specify the dataset path"
  exit
fi
dataset_path=$(readlink -f "$dataset_path")

echo "Docker version is ${version}"
echo "Dataset path is $(readlink -f "$dataset_path")"

open_explorer_path=$(readlink -f "$(dirname "$0")")
echo "OpenExplorer package path is $open_explorer_path"

if [ "$run_type" == "cpu" ];then
    echo "Start Docker container in CPU mode."
    docker run -it --rm \
      --hostname "OE-X5-CPU-$host_name" \
      --name $container_name \
      -v "$open_explorer_path":/open_explorer \
      -v "$dataset_path":/data/horizon_x5/data \
      openexplorer/ai_toolchain_ubuntu_20_x5_cpu:"$version"
else
    echo "Start Docker container in GPU mode."
    docker run -it --rm \
      --hostname "OE-X5-GPU-$host_name" \
      --name $container_name \
      --gpus all \
      --shm-size="15g" \
      -v "$open_explorer_path":/open_explorer \
      -v "$dataset_path":/data/horizon_x5/data \
      openexplorer/ai_toolchain_ubuntu_20_x5_gpu:"$version"
fi

③:在home下创建一个容器挂载时的文件夹:RDK_X5_related

④:把你转换得到的onnx模型放到 RDK_X5_related 文件夹中

请添加图片描述

⑤:在 RDK_X5_related 文件夹下再创建一个 cal_data_knob 文件夹,用于存放量化校正数据集

⑥:在数据集中挑选一部分图片,大概50张,放到 cal_data_knob (这里可以对应你自己创建的文件夹名称)文件夹下,如下所示:

⑦:在 RDK_X5_related 文件夹下再创建一个 yolov8_config.yaml 文件:

⑧:修改 yolov8_config.yaml 内容,设置好你的onnx模型名以及bin模型名前缀,尽量保持一致,如下所示:

yolov8_config.yaml 代码内容如下所示:

yaml 复制代码
model_parameters:
  # 你的 ONNX 模型文件名
  onnx_model: 'yolov8npose_knob.onnx'
  # RDK X5 的 BPU 微架构必须指定为 bayes-e
  march: 'bayes-e'
  layer_out_dump: False
  # 编译产物的工作目录
  working_dir: 'model_output'
  # 生成的 bin 模型名称前缀
  output_model_file_prefix: 'yolov8npose_knob'

input_parameters:
  input_name: ""
  # 推荐在板端实际推理时使用的输入格式(NV12 内存占用小,BPU 处理最快)
  input_type_rt: 'nv12'
  # 你在 PyTorch 训练时的图像格式和排布
  input_type_train: 'rgb'
  input_layout_train: 'NCHW'
  # 归一化方式。YOLOv8 默认是将 0-255 的像素值除以 255
  norm_type: 'data_scale'
  scale_value: 0.003921568627451  # 1 / 255.0 的精确值

calibration_parameters:
  # 刚才存放校准图片的相对路径
  cal_data_dir: './cal_data_knob'
  cal_data_type: 'float32'
  # 开启自动预处理,工具链会自动用 OpenCV 把你的图片 resize 到 640x640
  preprocess_on: True
  # 量化校准策略,默认使用 max 即可
  calibration_type: 'max'

compiler_parameters:
  # 编译优化策略:latency 优先保证单帧推理延迟最低
  compile_mode: 'latency'
  debug: False
  # 开启最高级别的图优化
  optimize_level: 'O3'

⑨:在horizon_x5_open_explorer_v1.2.8-py310_20240926路径下执行容器运行脚本:

bash 复制代码
sudo bash run_docker.sh /home/yjh/RDK_X5_related cpu

如下所示:

虚拟机内执行完脚本后进入容器了,ls结果:

进入容器后进入挂载路径 /data/horizon_x5/data

可以看到挂载路径下的内容和我们home下的 rdk_x5_related 文件夹下的内容是一模一样的:

七、容器内ONNX量化bin

在完成第六章的最后一步后,直接在容器内的 /data/horizon_x5/data 路径下,调用OE工具链检查onnx模型格式是否正确,注意复制代码时候要改成自己的模型名,如下所示:

一、ONNX模型检查

bash 复制代码
hb_mapper checker --model-type onnx --march bayes-e --model yolov8npose_knob.onnx


二、开始执行模型量化:

bash 复制代码
hb_mapper makertbin --model-type onnx --config yolov8_config.yaml

如下所示:


↑可以看到终端输出,模型量化成功

此时,量化好后的bin文件后放在自动生成的RDK_X5_related/model_output文件夹下

三、在量化生成的.bin模型基础上,剪除其反量化节点:
注意,反量化节点可能有点刚接触的同学不太熟悉,底层数学原理就不一一介绍了,直接按博主流程操作即可

1、查询.bin模型中能够删除的反量化节点名称:

bash 复制代码
hb_model_modifier yolov8npose_knob.bin

如下所示:

2、执行删除命令,注意要把模型名以及对应的节点名改成自己的:

bash 复制代码
hb_model_modifier yolov8npose_knob.bin \
   -r output0_HzDequantize \
   -r 294_HzDequantize \
   -r 300_HzDequantize \
   -r 307_HzDequantize \
   -r 312_HzDequantize \
   -r 318_HzDequantize \
   -r 325_HzDequantize \
   -r 330_HzDequantize \
   -r 336_HzDequantize

如下所示:

↑可以看到,已经生成了yolov8npose_knob_modified.bin,这里modified的意思就是指去除了反量化节点后的.bin模型,而且这个xxx_modified.bin模型同样保存在 model_output 文件夹下:

然后现在可以把yolov8npose_knob.bin和yolov8npose_knob_modified.bin这两个模型都复制到PC端本地了,然后上传到RDK X5开发板上

八、RDK X5边缘模型部署

在完成上述流程后,我们已经得到了符合要求的.bin以及xxx_modified.bin模型了,此时打开第三个项目文件,即博主的个人仓库,我已经把自己的yolov8npose_knob.bin和yolov8npose_knob_modified.bin这两个模型放到了Github项目的model文件夹下、测试图片放到inputimage文件夹下,大家 git clone 后可直接先把编译的相关内容删掉然后重新编译,再用我的模型和图片直接运行测试

项目地址:YOLOv8_RDKX5_object_pose

如果项目对大家有所帮助,希望点个免费的小星星:

git clone后把项目复制到开发板上,按如下流程操作:

如果需要直接测试博主的模型效果,如下所示:

可以在进入build文件夹,直接执行:

bash 复制代码
./rdkx5_yolov8_pose

查看运行结果。


如果使用自己的模型,修改流程如下所示:

①:cd build,删除所有build文件夹下的内容

②:cd src 修改main.cc,修改main函数中的如下几处内容:

先修改模型路径名、输入图片文件夹路径、输出图片文件夹路径、以及REMOVE_DEQUANT_NODE参数
这里着重说一下REMOVE_DEQUANT_NODE参数,
如果你加载的模型你是带反量化节点的模型,即xxx.bin,则REMOVE_DEQUANT_NODE设置为0或1都可以,
如果加载的是xxx_modified.bin,则REMOVE_DEQUANT_NODE必须设置为1

再修改CLASSES_NUM、NMS阈值、得分阈值:

再定义关键点名称:

修改完成后,保存main.cc

再删除inputimage和outputimage下的所有图片,然后将你要批量检测的图片放到inputimage下后,

在build路径下执行如下命令:

bash 复制代码
cmake ..
bash 复制代码
make
bash 复制代码
./rdkx5_yolov8_pose

终端结果如下所示:

注意,此时使用的是xxx.bin模型跑出的结果。

现在换成xxx_modified.bin模型后,修改REMOVE_DEQUANT_NODE参数后再次make后运行,如下所示:

两次结果对比可以明显看到,去除了反量化节点后的xxx_modified.bin模型的推理速度明显要快于xxx.bin模型,所以说xxx.bin模型和xxx_modified.bin模型都可以用,但是xxx_modified.bin模型对RDKX5上的Sunrise5 SoC更快,优化了大量的总线带宽被严重挤占的问题,对于开发板整体运行情况有较大改善。

以下是博主在RDKX5上完成推理自测后的结果图:

原图1:

结果图1:

原图2:

结果图2:

原图3:

结果图3:

上述即博主此次更新的YOLOv8pose部署地瓜RDK X5适配Sunrise 5 SoC的全部流程,包含PT转ONNX转.bin/modified.bin的完整步骤,欢迎交流!

相关推荐
爱学习的张大1 小时前
具身智能论文精度(八):Pi0.6
人工智能·深度学习
li1670902702 小时前
第二十七章:智能指针
c语言·数据结构·c++·visual studio
墨北小七2 小时前
从目标检测到行为识别:YOLO 模型微调实战
人工智能·深度学习·神经网络
gqk012 小时前
【无标题】
python
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【贪心与二分判定】:数列分段 Section II
c++·算法·贪心·csp·信奥赛·二分判定·数列分段 section ii
zh_xuan2 小时前
libcurl调用https接口
c++·libcurl
就叫飞六吧2 小时前
QT写一个桌面程序exe并动态打包基本流程(c++)
开发语言·c++
蜡笔小马3 小时前
1.c++设计模式-工厂模式
c++
V搜xhliang02463 小时前
OpenClaw科研全场景用法:从文献到实验室的完整自动化方案
运维·开发语言·人工智能·python·算法·microsoft·自动化