YOLOv8目标检测算法移植

YOLOv8与YOLOv5的异同

  1. YOLOv8与YOLOv5的网络结构相似, 缺省仍然采用三尺度检测
  2. YOLOv8的Backbone用C2f替换C3结构,仍使用残差结构, 但输入被分为两半, 一半用采用与C3相同的残差结构, 另一半直通.
  3. YOLOv8采用anchor-free模型, 使用DFL损失函数, Detect用DFL计算bounding box
  4. YOLOv8取消目标置信度,直接使用分类置信度(目标置信度存在信息冗余)

C2f中使用了split和concat算子,需要进行大量内存操作,对有些NPU实现不友好(不支持或者速度慢)。

YOLOv5/8的Backbone仅使用基础算子如Conv2D、Batch Normalization、Sigmoid、Split、Concat、Max Pooling、Upsample等,主流的NPU都能支持,不是移植关注的重点。

目标置信度

YOLOv5的后处理先对目标置信度进行筛选,再搜索分类置信度中的最大值作为分类置信度,用目标置信度x分类置信度得到最终的置信度。

YOLOv8取消了目标置信度,因此要先搜索分类置信度的最大值,再进行筛选。

目标框(bonding box)

目标框的计算公式与损失函数中目标框损失有对应关系。

  1. YOLOv5

YOLOv5检测头对每个GRID回归得到以GRID中心为原点的目标框中心点位置(xy)和基于anchor的目标框尺寸(wh)。

小目标的目标框尺寸的偏移1像素产生的误差,比大目标的目标框尺寸偏移1像素产生的误差要大。

出于平衡小目标和大目标的目标框尺寸的误差的需要,YOLOv5的目标框损失函数对目标框尺寸(wh)取平方根,这也是YOLOv5检测头计算wh时取平方的原因。[参见YOLOv5算法移植后处理中的转换bouding box]

  1. YOLOv8

YOLOv8为解决anchor-base问题,使用DFL Loss,对xyxy(左上角坐标和右下角坐标)进行回归并计算损失。其本质是对两个点四个坐标值进行如下处理:

  • 当作分类问题,计算0~15共16个分类(bouding box的16分类是YOLOv8的缺省超参数,根据目标框的聚合情况可以进行修改)的分类概率
  • 再使用softmax将分类问题转换为回归问题,计算出最终的坐标值

对于左上角坐标,0~15表示以当前GRID中心点为基准,向左或向上偏移0~15个GRID;
对于右下角坐标,0~15表示以当前GRID中心点为基准,向右或向下偏移0~15个GRID

导出和裁剪

与YOLOv5类似,YOLOv8的Detect层仅cv2和cv3有需要训练的权重,分别对应分类置信度和bounding box。
后面的计算更多地是为了方便利用numpy的向量计算,但是增加了大量concat、split、常量和乘除法。这些算子有一些可能不被NPU加速器支持,即使支持必要性也不大,因此全部删除。

从github下载YOLOv8源代码

修改文件ultralytics/nn/modules/head.pyDetect类的forward函数:

    def forward(self, x):
        """Concatenates and returns predicted bounding boxes and class probabilities."""
        export_cut = []
        for i in range(self.nl):
        	#### 删除开始 ####
            #x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1)
        	#### 删除结束 ####
            #### 增加开始 ####
            cv2, cv3 = self.cv2[i](x[i]), self.cv3[i](x[i])
            if self.export:
                # cv2(分类置信度)添加sigmoid,这里的sigmoid也可以不要,在后处理中计算
                # cv3(bounding box)添加sigmoid
                # cv2和cv3都进行转置,转置前通道数位于第二维度,方便进行Conv2D运算
                # 转置后通道数位于第四维度,符合C/C++数组定义的习惯,方便C/C++代码计算缓存偏移
                # 上一篇文章中YOLOv5导出裁剪时没有进行转置,C/C++代码计算缓存偏移有些别扭
                export_cut += [cv2.permute(0, 2, 3, 1), cv3.sigmoid().permute(0, 2, 3, 1)]
            x[i] = torch.cat((cv2, cv3), 1)
            #### 增加结束 ####
        #### 增加开始 ####
        if self.export:
        	return export_cut
        #### 增加结束 ####
        #### 以下不变 ####
        if self.training:  # Training path
            return x

在根目录下添加文件yolo,添加以下内容

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from ultralytics.cfg import entrypoint
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(entrypoint())

执行脚本导出onnx模型文件

python3 yolo export detect model=yolov8n.pt format=onnx imgsz=416,736 opset=12

参数可通过执行python3 yolo查看,与YOLOv5导出脚本的参数含义基本一致。

YOLOv8代码的封装层级比YOLOv5多,程序执行流程不太直观,多借助python调试工具单步执行有助于熟悉代码。

测试

加载模型文件、测试图片并运行与YOLOv5模型验证完全相同,不再重复。

后处理

YOLOv8是anchor-free模型,不需要从数据集中聚类anchor,后处理过程与输入图像的尺寸也不直接相关。

  1. 用DFL计算bounding box

代码如下:

softmax函数

def softmax(x, axis=-1):
    e = np.exp(x)
    return e / np.sum(e, axis=axis).reshape(-1, 1)

dfl函数:从模型输出的bounding box特征图计算bounding box的回归值

def dfl(bbox):
    '''从模型输出的bounding box特征图计算bounding box的回归值
    '''
	# bounding box输出缓存的最后一个维度大小默认为64 = 4 * 16
	n = bbox.shape[-1]
	a = np.arrange(n // 4)
	s = softmax(bbox.reshape(4, n // 4), axis=-1)
	d = s.dot(a)
	return d

坐标转换函数: 从bouding box回归值计算bounding box的归一化坐标

def conv(b, row, col, w, h):
	'''从bouding box回归值计算bounding box的归一化坐标

	b: bounding box的回归值
	row: 当前grid的行序号
	col: 当前grid的列序号
	w: 当前特征图的宽度
	h: 当前输出特征图的高度

	左上角回归值相对于grid中心点作副偏移
	右下角回归值相对于grid中心点作正偏移
	```
    x1 = (col + 0.5 - d[0]) / w
    y1 = (row + 0.5 - d[1]) / h
    x2 = (col + 0.5 + d[2]) / w
    y2 = (row + 0.5 + d[3]) / h
    return x1, y1, x2, y2
  1. 除DFL外,YOLOv8的后处理与YOLOv5基本一样,区别在于:

由于YOLOv8是anchor-free模型,因此不像YOLOv5的每个输出尺度中需要分三种anchor分别对每个grid进行过滤和转换,也就是YOLOv8的后处理少了anchor这一层循环,仅需要处理batch, height, width三层循环。不过借助numpy的向量化处理能力,python代码仅需要正确设置特征图的维度,不需要显式书写循环。

由于YOLOv8取消了目标置信度,因此用类别置信度与置信度阈值进行比较即可。

模型转换

YOLOv8和YOLOv5的模型转换过程相同,需要注意的地方也相同。

C/C++调用代码

模型加载、图片加载、分配缓存、运行模型等主要与处理器(AI库)相关,与模型关系不大。

后处理部分与YOLOv5的后处理的差别主要是DFL。

float yolov8cut_convertor::dfl(const int16_t* buf, float scale)
{
    // YOLOv8的DFL实现为softmax + 1x1卷积
    // 输入通道数为16, 输出通道为1, 卷积权重为[0...15],权重不需要学习
    // 实际上dfl是16分类问题通过softmax转换为回归问题
    // 这里实现时先算自然指数, 分别求softmax和dfl卷积和, 再相除得到最后结果
    // 这么计算可以减少一次循环和15次浮点除法
    float d[BBOX_DATA_LENGTH];
    for (ulong i = 0; i < BBOX_DATA_LENGTH; ++i) {
        d[i] = buf[i] * scale;
    }
    float sum_sm = 0; // softmax的分母
    float sum_dfl = 0; // (dfl+softmax)的分子
    for (ulong i = 0; i < BBOX_DATA_LENGTH; ++i) {
        float e = exp(d[i]);
        sum_sm += e;
        sum_dfl += e * i;
    }
    return sum_dfl / sum_sm;
}

将bounding box从回归值转换为归一化坐标的代码与python代码相同

common::xyxy yolov8cut_convertor::convert_box(const int16_t* bb, ulong col, ulong row, ulong w, ulong h, float scale)
{
    float d[4];
    for (ulong i = 0; i < sizeof(d) / sizeof(d[0]); ++i) {
        d[i] = this->dfl(&bb[i * BBOX_DATA_LENGTH], scale);
    }
    float x1 = (col + 0.5 - d[0]) / w;
    float y1 = (row + 0.5 - d[1]) / h;
    float x2 = (col + 0.5 + d[2]) / w;
    float y2 = (row + 0.5 + d[3]) / h;
    return common::xyxy{x1, y1, x2, y2};
}

整体转换过程与python代码相同,按照batch、height、weight三层循环逐层计算

1. 查找并计算分类置信度,并记录类别索引,乘以量化系数,如果小于阈值则忽略
2. 计算并保存bounding box、类别索引、置信度

完成过滤和转换之后,分类对bounding box进行NMS

下节预告:YOLOv8支持关键点(人体姿态/骨骼点)检测。关键点检测的Backbone和Neck与目标检测相同,在Head网络增加关键点预测分支,并在目标检测损失函数的基础上融合关键点损失,使得YOLOv8具备关键点检测能力。YOLOv8的关键点是xyc三元组,分别表示坐标xy和置信度。关键点检测除了可用于人体姿态估计之外,也可以用于人脸检测、车牌定位等场景。后续文章会描述在YOLOv8目标检测模型移植的基础上实现关键点检测模型的移植细节。

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值