Tutorial_Cpp.md 8.89 KB
Newer Older
yaoht's avatar
yaoht committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# YOLOV8检测器

YOLOV8模型是目前工业界使用较多的算法,官方提供了多个不同版本的预训练模型,本份文档主要介绍了如何基于migraphx构建YOLOV8推理,包括:静态推理、动态shape推理,该示例推理流程对YOLOV8其他版本的模型同样适用。

## 模型简介

YOLOV8是一种单阶段目标检测算法,该算法在YOLOV5的基础上添加了一些新的改进思路,使其速度与精度都得到了极大的性能提升。具体包括:骨干网络和 Neck 部分可能参考了 YOLOv7 ELAN 设计思想,将 YOLOv5 的 C3 结构换成了梯度流更丰富的 C2f 结构,并对不同尺度模型调整了不同的通道数,属于对模型结构精心微调,不再是无脑一套参数应用所有模型,大幅提升了模型性能。Head 部分相比 YOLOv5 改动较大,换成了目前主流的解耦头结构,将分类和检测头分离,同时也从 Anchor-Based 换成了 Anchor-Free。Loss 计算方面采用了 TaskAlignedAssigner 正样本分配策略,并引入了 Distribution Focal Loss。训练的数据增强部分引入了 YOLOX 中的最后 10 epoch 关闭 Mosiac 增强的操作,可以有效地提升精度。网络结构如图所示。

<img src=./yolov8_model.jpg style="zoom:100%;" align=middle>

## 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorYOLOV8节点表示YOLOV8检测器的参数,相关参数主要依据官方推理示例进行设置。各个参数含义如下:

- ModelPathDynamic:yolov8动态模型存放路径
- ModelPathStatic:yolov8静态模型存放路径
- ClassNameFile:coco数据集类别文件存放路径
- UseFP16:是否使用FP16推理模式
- NumberOfClasses:检测类别数量
- ConfidenceThreshold:置信度阈值,用于判断proposal内的物体是否为正样本
- NMSThreshold:非极大值抑制阈值,用于消除重复框

```yaml
<ModelPathDynamic>"../Resource/Models/yolov8n_dynamic.onnx"</ModelPathDynamic>
<ModelPathStatic>"../Resource/Models/yolov8n_static.onnx"</ModelPathStatic>
<ClassNameFile>"../Resource/Models/coco.names"</ClassNameFile>
<UseFP16>0</UseFP16><!--是否使用FP16-->
<NumberOfClasses>80</NumberOfClasses><!--类别数(不包括背景类),COCO:80,VOC:20-->
<ConfidenceThreshold>0.5</ConfidenceThreshold>
<NMSThreshold>0.5</NMSThreshold>
```

## 模型初始化

模型初始化首先通过parse_onnx()函数加载YOLOV8的onnx模型。

- 静态推理:调用parse_onnx函数对静态模型进行解析

```cpp
ErrorCode DetectorYOLOV8::Initialize(InitializationParameterOfDetector initializationParameterOfDetector, bool dynamic)
{
    ...
yaoht's avatar
yaoht committed
43
44
45
46

    // 加载模型
    net = migraphx::parse_onnx(modelPath);
    LOG_INFO(stdout,"succeed to load model: %s\n",GetFileName(modelPath).c_str());
yaoht's avatar
yaoht committed
47
48
49
50
51
52
53
54
55
56
    ...
    
}
```

- 动态shape推理:需要设置模型输入的最大shape,本示例设为{1,3,1024,1024}

```cpp
ErrorCode DetectorYOLOV8::Initialize(InitializationParameterOfDetector initializationParameterOfDetector, bool dynamic)
{
yaoht's avatar
yaoht committed
57
58
59
60
61
62
    ...
    
    migraphx::onnx_options onnx_options;
    onnx_options.map_input_dims["images"]={1,3,1024,1024};// 
    net = migraphx::parse_onnx(modelPath, onnx_options);
    
yaoht's avatar
yaoht committed
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    ...
}
```

## 预处理

在将数据输入到模型之前,需要对图像做如下预处理操作:

- 转换数据排布为NCHW

- 归一化[0.0, 1.0]

- 输入数据的尺寸变换:静态推理将输入大小固定为relInputShape=[1,3,640,640],动态推理对输入图像尺寸变换为设定的动态尺寸。

```cpp
ErrorCode DetectorYOLOV8::Detect(const cv::Mat &srcImage, std::vector<std::size_t> &relInputShape, std::vector<ResultOfDetection> &resultsOfDetection, bool dynamic)
{ 
yaoht's avatar
yaoht committed
80
    ...
yaoht's avatar
yaoht committed
81
  
yaoht's avatar
yaoht committed
82
    // 数据预处理并转换为NCHW格式
yaoht's avatar
yaoht committed
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
    inputSize = cv::Size(relInputShape[3], relInputShape[2]);
    cv::Mat inputBlob;
    cv::dnn::blobFromImage(srcImage,
                    inputBlob,
                    1 / 255.0,
                    inputSize,
                    cv::Scalar(0, 0, 0),
                    true,
                    false);
                    
   ...
}
```

## 推理

yaoht's avatar
yaoht committed
99
利用migraphx推理得到YOLOV8模型的输出。其中静态推理输入数据inputData的shape大小为模型的固定输入尺寸,动态推理则为实际输入的尺寸。
yaoht's avatar
yaoht committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139

```cpp
ErrorCode DetectorYOLOV8::Detect(const cv::Mat &srcImage, std::vector<std::size_t> &relInputShape, std::vector<ResultOfDetection> &resultsOfDetection, bool dynamic)
{	
	...
	
	// 创建输入数据
    migraphx::parameter_map inputData;
    if(dynamic)
    {
        inputData[inputName]= migraphx::argument{migraphx::shape(inputShape.type(), relInputShape), (float*)inputBlob.data};
    }
    else
    {
        inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
    }
    

    // 推理
    std::vector<migraphx::argument> inferenceResults = net.eval(inputData);
    
    ...
    
}
```

YOLOV8的MIGraphX推理结果inferenceResults是一个std::vector< migraphx::argument >类型,YOLOV8的onnx模型包含一个输出,所以result等于inferenceResults[0],result包含三个维度:outputShape.lens()[0]=1表示batch信息,outputShape.lens()[1]=84表示对每个proposal的预测信息。同时可将84拆分为4+80,前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框,最后80个参数用于判断每一个特征点所包含的物体种类,outputShape.lens()[2]=8400表示生成proposal数量。

另外,如果想要指定输出节点,可以在eval()方法中通过提供outputNames参数来实现:

```cpp
...
// 推理
std::vector<std::string> outputNames = {"output0"}
std::vector<migraphx::argument> inferenceResults = net.eval(inputData, outputNames);
...
```

如果没有指定outputName参数,则默认输出所有输出节点,此时输出节点的顺序与ONNX中输出节点顺序保持一致,可以通过netron查看ONNX文件的输出节点的顺序。

yaoht's avatar
yaoht committed
140
获取上述信息之后进行proposal筛选,筛选过程根据confidenceThreshold阈值进行筛选,proposal的最大置信度得分maxClassScore由80个类别的分数最高值确定,当maxClassScore大于confidenceThreshold阈值,则进一步获取当前proposal的坐标信息和预测物体类别信息,小于该阈值则不做处理。
yaoht's avatar
yaoht committed
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

```cpp
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<std::size_t> &relInputShape, std::vector<ResultOfDetection> &resultsOfDetection, bool dynamic)
{

	...
	//获取先验框的个数
    int numProposal = outs[0].size[2];
    int numOut = outs[0].size[1];

    //变换输出的维度
    outs[0] = outs[0].reshape(1, numOut);
    cv::transpose(outs[0], outs[0]);

    float *data = (float *)outs[0].data;

    //生成先验框
    std::vector<float> confidences;
    std::vector<cv::Rect> boxes;
    std::vector<int> classIds;
    float ratioh = (float)srcImage.rows / inputSize.height, ratiow = (float)srcImage.cols / inputSize.width;

    //计算x,y,w,h
    for (int n = 0; n < numProposal; n++)
    {
        float *classes_scores = data+4;
        cv::Mat scores(1, classNames.size(), CV_32FC1, classes_scores);

        cv::Point class_id;
        double maxClassScore;
        cv::minMaxLoc(scores, 0, &maxClassScore, 0, &class_id);
        if (maxClassScore > yolov8Parameter.confidenceThreshold)
        {
            confidences.push_back(maxClassScore);
            classIds.push_back(class_id.x);

            float x = data[0];
            float y = data[1];
            float w = data[2];
            float h = data[3];

            int left = int((x - 0.5 * w) * ratiow);
            int top = int((y - 0.5 * h) * ratioh);

            int width = int(w * ratiow);
            int height = int(h * ratioh);

            boxes.push_back(cv::Rect(left, top, width, height));
        }
        data += numOut;
    }
	
	...
}
```

为了消除重叠锚框,输出最终的YOLOV8目标检测结果,执行非极大值抑制对筛选之后的proposal进行处理,最后保存检测结果到resultsOfDetection中。

```cpp
ErrorCode DetectorYOLOV5::Detect(const cv::Mat &srcImage, std::vector<std::size_t> &relInputShape, std::vector<ResultOfDetection> &resultsOfDetection, bool dynamic)
{

	...
    
    // 执行non maximum suppression消除冗余重叠boxes
    std::vector<int> indices;
    cv::dnn::NMSBoxes(boxes, confidences, yolov5Parameter.confidenceThreshold, yolov5Parameter.nmsThreshold, indices);
    for (size_t i = 0; i < indices.size(); ++i)
    {
        int idx = indices[i];
        int classID=classIds[idx];
        string className=classNames[classID];
        float confidence=confidences[idx];
        cv::Rect box = boxes[idx];
		
        //保存每个最终预测proposal的坐标值、置信度分数、类别ID
        ResultOfDetection result;
        result.boundingBox=box;
        result.confidence=confidence;// confidence
        result.classID=classID; // label
        result.className=className;
        resultsOfDetection.push_back(result);
    }

    ...
}
```