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

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

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

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

文章目录

一、项目回顾

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

博主之前有写过在华为Ascend、瑞芯微RK系列SoC上的YOLOv8目标检测&图像分割、YOLOv10目标检测、MoblieNetv2图像分类的模型训练、转换、部署文章,感兴趣的小伙伴可以了解下:
【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_detect


三、模型训练

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

这是博主的train.py:

python 复制代码
from ultralytics import YOLO

# 加载模型
model = YOLO("/xxx/Algorithm/YOLOv8/ultralytics-main/yaml/yolov8n.yaml")  # 从头开始构建新模型
#model = YOLO("yolov8n.pt")  # 加载预训练模型(推荐用于训练)

# Use the yaml
results = model.train(data="/xxx/Algorithm/YOLOv8/ultralytics-main/yaml/bird.yaml", epochs=300, batch=32)  # 训练模型

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

这是模型训练结果:

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

四、PT转ONNX

pt转onnx时,需要修改自己的head.py的Detect类forward函数,目的是修改Detect 的输出头,直接将三个特征层的BoundingBox信息和Classify信息分开输出,一共6个输出头原

head.py原内容如下所示:

修改成如下所示:

修改完成后的完整Detect类如下所示,不会改的可以直接复制,但要记得在寻俩你的时候要把这个专用的 RDKX5 的forward注掉,改用原来的forward训练:

python 复制代码
class Detect(nn.Module):
    """YOLOv8 Detect head for detection models."""

    dynamic = False  # force grid reconstruction
    export = False  # export mode
    end2end = False  # end2end
    max_det = 300  # max_det
    shape = None
    anchors = torch.empty(0)  # init
    strides = torch.empty(0)  # init

    def __init__(self, nc=80, ch=()):
        """Initializes the YOLOv8 detection layer with specified number of classes and channels."""
        super().__init__()
        self.nc = nc  # number of classes
        self.nl = len(ch)  # number of detection layers
        self.reg_max = 16  # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
        self.no = nc + self.reg_max * 4  # number of outputs per anchor
        self.stride = torch.zeros(self.nl)  # strides computed during build
        c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100))  # channels
        self.cv2 = nn.ModuleList(
            nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch
        )
        self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
        self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

        if self.end2end:
            self.one2one_cv2 = copy.deepcopy(self.cv2)
            self.one2one_cv3 = copy.deepcopy(self.cv3)

    # def forward(self, x):
    #     """Concatenates and returns predicted bounding boxes and class probabilities."""
    #     if self.end2end:
    #         return self.forward_end2end(x)

    #     for i in range(self.nl):
    #         x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
    #     if self.training:  # Training path
    #         return x
    #     y = self._inference(x)
    #     return y if self.export else (y, x)
    
    # ★★★★★++++++++++++++++++++++ RDK X5 专用逻辑 ++++++++++++++++++++++★★★★★
    def forward(self, x):
        """Modified for Horizon RDK X5 (BPU) Deployment."""
        result = []
        for i in range(self.nl):
            # 输出 bbox 预测分支 (cv3)
            result.append(self.cv3[i](x[i]).permute(0, 2, 3, 1).contiguous())
            # 输出 class 预测分支 (cv2)
            result.append(self.cv2[i](x[i]).permute(0, 2, 3, 1).contiguous())
            
        # 返回 6 个独立的特征图 (3个尺度 x 2个分支)
        return result
    # ★★★★★++++++++++++++++++++++ RDK X5 专用逻辑 ++++++++++++++++++++++★★★★★

    def forward_end2end(self, x):
        """
        Performs forward pass of the v10Detect module.

        Args:
            x (tensor): Input tensor.

        Returns:
            (dict, tensor): If not in training mode, returns a dictionary containing the outputs of both one2many and one2one detections.
                           If in training mode, returns a dictionary containing the outputs of one2many and one2one detections separately.
        """
        x_detach = [xi.detach() for xi in x]
        one2one = [
            torch.cat((self.one2one_cv2[i](x_detach[i]), self.one2one_cv3[i](x_detach[i])), 1) for i in range(self.nl)
        ]
        for i in range(self.nl):
            x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
        if self.training:  # Training path
            return {"one2many": x, "one2one": one2one}

        y = self._inference(one2one)
        y = self.postprocess(y.permute(0, 2, 1), self.max_det, self.nc)
        return y if self.export else (y, {"one2many": x, "one2one": one2one})

    def _inference(self, x):
        """Decode predicted bounding boxes and class probabilities based on multiple-level feature maps."""
        # Inference path
        shape = x[0].shape  # BCHW
        x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2)
        if self.dynamic or self.shape != shape:
            self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5))
            self.shape = shape

        if self.export and self.format in {"saved_model", "pb", "tflite", "edgetpu", "tfjs"}:  # avoid TF FlexSplitV ops
            box = x_cat[:, : self.reg_max * 4]
            cls = x_cat[:, self.reg_max * 4 :]
        else:
            box, cls = x_cat.split((self.reg_max * 4, self.nc), 1)

        if self.export and self.format in {"tflite", "edgetpu"}:
            # Precompute normalization factor to increase numerical stability
            # See https://github.com/ultralytics/ultralytics/issues/7371
            grid_h = shape[2]
            grid_w = shape[3]
            grid_size = torch.tensor([grid_w, grid_h, grid_w, grid_h], device=box.device).reshape(1, 4, 1)
            norm = self.strides / (self.stride[0] * grid_size)
            dbox = self.decode_bboxes(self.dfl(box) * norm, self.anchors.unsqueeze(0) * norm[:, :2])
        else:
            dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides

        return torch.cat((dbox, cls.sigmoid()), 1)

    def bias_init(self):
        """Initialize Detect() biases, WARNING: requires stride availability."""
        m = self  # self.model[-1]  # Detect() module
        # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1
        # ncf = math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum())  # nominal class frequency
        for a, b, s in zip(m.cv2, m.cv3, m.stride):  # from
            a[-1].bias.data[:] = 1.0  # box
            b[-1].bias.data[: m.nc] = math.log(5 / m.nc / (640 / s) ** 2)  # cls (.01 objects, 80 classes, 640 img)
        if self.end2end:
            for a, b, s in zip(m.one2one_cv2, m.one2one_cv3, m.stride):  # from
                a[-1].bias.data[:] = 1.0  # box
                b[-1].bias.data[: m.nc] = math.log(5 / m.nc / (640 / s) ** 2)  # cls (.01 objects, 80 classes, 640 img)

    def decode_bboxes(self, bboxes, anchors):
        """Decode bounding boxes."""
        return dist2bbox(bboxes, anchors, xywh=not self.end2end, dim=1)

    @staticmethod
    def postprocess(preds: torch.Tensor, max_det: int, nc: int = 80):
        """
        Post-processes YOLO model predictions.

        Args:
            preds (torch.Tensor): Raw predictions with shape (batch_size, num_anchors, 4 + nc) with last dimension
                format [x, y, w, h, class_probs].
            max_det (int): Maximum detections per image.
            nc (int, optional): Number of classes. Default: 80.

        Returns:
            (torch.Tensor): Processed predictions with shape (batch_size, min(max_det, num_anchors), 6) and last
                dimension format [x, y, w, h, max_class_prob, class_index].
        """
        batch_size, anchors, predictions = preds.shape  # i.e. shape(16,8400,84)
        boxes, scores = preds.split([4, nc], dim=-1)
        index = scores.amax(dim=-1).topk(min(max_det, anchors))[1].unsqueeze(-1)
        boxes = boxes.gather(dim=1, index=index.repeat(1, 1, 4))
        scores = scores.gather(dim=1, index=index.repeat(1, 1, nc))
        scores, index = scores.flatten(1).topk(max_det)
        i = torch.arange(batch_size)[..., None]  # batch indices
        return torch.cat([boxes[i, index // nc], scores[..., None], (index % nc)[..., None].float()], dim=-1)

在之前第三步的路径下使用pt2onnx.py将自己的pt模型转成onnx格式,博主的pt2onnx.py如下所示:

python 复制代码
from ultralytics import YOLO

# 加载训练好的模型
model = YOLO("birds_yolov8_best.pt")  #相对路径

# 导出为 ONNX 格式
model.export(
    format="onnx",
    imgsz=640,          # 输入尺寸(与训练一致)
    opset=11,           # ONNX 算子集版本(建议12+)
    dynamic=False,       # 是否启用动态维度(True 适用于可变输入尺寸)
    simplify=False,       # 启用 ONNX Simplifier 优化模型
    task="detect",    # 指定任务类型(目标检测)
)

执行转换命令:

bash 复制代码
python pt2onnx.py

终端结果如下所示:

作为对比,这是使用head.py中原forward函数转换的结果:

可以看到,不修改输出头的话,所有的信息全部都在一个输出tensor里,不利于后续的模型量化和板端推理。

执行完pt2onnx.py后,在当前路径下生成的onnx:

此时再可视化对比下修改了head.py和未修改head.py生成的onnx的二者区别对比:

拆分后ONNX的netron可视化效果:

未拆分后ONNX的netron可视化效果:

和终端中显示的输出tensor一致。

五、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 文件夹,用于存放量化校正数据集

⑥:在数据集中挑选一部分图片,大概50张,放到 cal_data 文件夹下,如下所示:

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

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

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

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

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'
  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模型格式是否正确,注意复制代码时候要改成自己的模型名,如下所示:

bash 复制代码
hb_mapper checker --model-type onnx --march bayes-e --model birds_yolov8_best.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 birds_yolov8_best.bin

如下所示:

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

bash 复制代码
hb_model_modifier birds_yolov8_best.bin \
  -r output0_HzDequantize \
  -r 276_HzDequantize \
  -r 283_HzDequantize \
  -r 288_HzDequantize \
  -r 295_HzDequantize \
  -r 300_HzDequantize

如下所示:

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

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

八、RDK X5边缘模型部署

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

项目地址:https://github.com/A7bert777/YOLOv8_RDKX5_object_detect

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

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

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

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

bash 复制代码
./rdk_yolov8_detect

查看运行结果。


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

①: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阈值、得分阈值:

再修改类别名:博主模型只识别一个类别:"bird",如果要识别多个类别,后面加","后换行

修改完成后,保存main.cc

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

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

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

终端结果如下所示:

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

现在换成xxx.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:

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

相关推荐
weixin_580614002 小时前
如何防止SQL注入篡改数据_实施双重身份验证与授权
jvm·数据库·python
2401_897190552 小时前
SQL视图占空间吗_理解视图定义与存储机制的底层逻辑
jvm·数据库·python
墨心@2 小时前
多Agent系统的编排
人工智能·语言模型·自然语言处理·agent·datawhale·agent设计模式·组队学习
qq_424098562 小时前
C#怎么实现UDP广播通信_C#如何搭建Socket网络【核心】
jvm·数据库·python
2501_914245932 小时前
Python Web开发如何防范SQL注入_使用参数化查询与ORM实践
jvm·数据库·python
向量引擎2 小时前
AI要干掉全公司只剩老板一人这事到底靠不靠谱
人工智能·aigc·agi·api调用
yejqvow122 小时前
Golang怎么做模糊测试fuzz_Golang Fuzz测试教程【高效】
jvm·数据库·python
这儿有一堆花2 小时前
实用CLI工具:命令行下的高效选择
人工智能
2401_897190552 小时前
mysql如何通过mysqldump备份视图与触发器_使用相关参数
jvm·数据库·python