RetinaFace.md 11.4 KB
Newer Older
lijian6's avatar
Update  
lijian6 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
140
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# RetinaFace人脸检测器

## 模型简介

RetinaFace是一个经典的人脸检测模型(https://arxiv.org/abs/1905.00641),采用了SSD架构。

![image-20221215140647406](../Images/RetinaFace_01.png)

本示例采用了如下的开源实现:https://github.com/biubug6/Pytorch_Retinaface,作者提供了restnet50 和mobilenet0.25两个预训练模型,本示例使用了mobilenet0.25预训练模型,将mobilenet0.25预训练模型下载下来后,保存到Pytorch_Retinaface工程的weights目录。



## 模型转换

在将mobilenet0.25预训练模型转换为onnx文件的时候,本示例需要对作者提供的python代码做如下改变:

### 修改models/retinaface.py

1. **将ClassHead类修改为如下实现**

   ```
   class ClassHead(nn.Module):
       def __init__(self,inchannels=512,num_anchors=3):
           super(ClassHead,self).__init__()
           self.num_anchors = num_anchors
           self.conv1x1 = nn.Conv2d(inchannels,self.num_anchors*2,kernel_size=(1,1),stride=1,padding=0)
   
       def forward(self,x):
           out = self.conv1x1(x)
           
           return out
   ```

   由于本示例的C++推理代码已经实现了permute操作,所以这里需要去掉out.permute(0,2,3,1).contiguous()。

2. **将BboxHead类修改为如下实现**

   ```
   class BboxHead(nn.Module):
       def __init__(self,inchannels=512,num_anchors=3):
           super(BboxHead,self).__init__()
           self.conv1x1 = nn.Conv2d(inchannels,num_anchors*4,kernel_size=(1,1),stride=1,padding=0)
   
       def forward(self,x):
           out = self.conv1x1(x)
   
           return out
   ```

   与ClassHead一样,需要去掉permute操作。

3. **将RetinaFace类的forward修改为如下实现**

   ```
   def forward(self,inputs):
           out = self.body(inputs)
   
           # FPN
           fpn = self.fpn(out)
   
           # SSH
           feature1 = self.ssh1(fpn[0])
           feature2 = self.ssh2(fpn[1])
           feature3 = self.ssh3(fpn[2])
           features = [feature1, feature2, feature3]
   
           bbox_regressions = [self.BboxHead[i](feature) for i, feature in enumerate(features)]
           classifications = [self.ClassHead[i](feature) for i, feature in enumerate(features)]
          
           output=(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2])
   
           return output
   ```

   本示例去掉了landmark检测功能,所以需要去掉forward中的landmark部分,bbox_regressions和classifications需要删除torch.cat操作,同时需要修改output为(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2])。



### 修改data/config.py

将cfg_mnet中的'pretrain': True,修改为'pretrain': False,



### 修改convert_to_onnx.py

导出onnx模型的时候,需要修改原来的output_names,可以直接删除torch.onnx._export()的output_names参数或者手动指定每个输出节点的名字,如果直接删除了output_names参数,则会生成一个随机名,本示例直接删除了output_names参数,同时本示例修改了onnx文件名output_onnx,修改后的main函数如下:

```
if __name__ == '__main__':
    torch.set_grad_enabled(False)
    cfg = None
    if args.network == "mobile0.25":
        cfg = cfg_mnet
    elif args.network == "resnet50":
        cfg = cfg_re50
    # net and model
    net = RetinaFace(cfg=cfg, phase = 'test')
    net = load_model(net, args.trained_model, args.cpu)
    net.eval()
    print('Finished loading model!')
    print(net)
    device = torch.device("cpu" if args.cpu else "cuda")
    net = net.to(device)

    # ------------------------ export -----------------------------
    output_onnx = 'mobilenet0.25_Final.onnx'
    print("==> Exporting model to ONNX format at '{}'".format(output_onnx))
    input_names = ["input0"]
    output_names = ["output0"]
    inputs = torch.randn(1, 3, args.long_side, args.long_side).to(device)

    torch_out = torch.onnx._export(net, inputs, output_onnx, export_params=True, verbose=False,
                                   input_names=input_names)
```

注意:如果需要修改模型的输入大小,可以修改args.long_side参数,默认为640x640。



完成上述修改后,执行python convert_to_onnx.py命令就可以实现模型转换了,转换成功后会在当前目录生成mobilenet0.25_Final.onnx文件,下面就可以进行推理了。本示例将修改好的工程保存到了samples工程中的Resource/Models/Detector/RetinaFace目录中,在Pytorch_Retinaface目录中执行python convert_to_onnx.py命令可以直接生成onnx文件。



## 检测器参数设置

samples工程中的Resource/Configuration.xml文件的DetectorRetinaFace节点表示RetinaFace检测器的参数,这些参数是根据Pytorch_Retinaface工程中的data/config.py文件中的cfg_mnet来设置的,下面我们看一下是如何通过cfg_mnet来设置的。

2. **设置anchor大小**
   cfg_mnet的min_sizes表示每一个priorbox层的anchor大小,我们可以看到该模型一共有3个priorbox层,第一层anchor大小为16和32,第二层anchor大小为64和128,第三层anchor大小为256和512,注意:**Configuration.xml中priorbox层的顺序要与onnx文件中的输出节点顺序保持一致**,通过netron (https://netron.app/) 可以看到首先输出的是467和470节点,这两个节点对应的是特征图最大的检测层,所以对应的anchor大小为16和32,最后输出的是469和472节点,这两个节点对应的是特征图最小的检测层,所以对应的anchor大小为256和512,

   ![image-20221215153957174](../Images/RetinaFace_02.png)
   
   
   所以Configuration.xml配置文件中的参数设置如下:
   
   ```
   <!--priorbox层的个数-->
   <PriorBoxLayerNumber>3</PriorBoxLayerNumber>
   
   <!--每个priorbox层的minisize-->
   <MinSize11>16</MinSize11>
   <MinSize12>32</MinSize12>
   <MinSize21>64</MinSize21>
   <MinSize22>128</MinSize22>
   <MinSize31>256</MinSize31>
   <MinSize32>512</MinSize32>
   ```
   
3. **设置Flip和Clip**
   cfg_mnet中的clip为False,所以Configuration.xml中对应的参数设置为0即可,由于只有一个宽高比为1的anchor,所以Flip设置为0。

   ```
   <Flip1>0</Flip1>
   <Flip2>0</Flip2>
   <Flip3>0</Flip3>
   
   <Clip1>0</Clip1>
   <Clip2>0</Clip2>
   <Clip3>0</Clip3>
   ```

4. **设置anchor的宽高比**
   由于RetinaFace只包含宽高比为1的anchor,所以这里不需要设置宽高比。

5. **设置每个priorbox层的步长**
   cfg_mnet中的steps表示每个priorbox层的步长,所以三个priorbox的步长依次为8,16,32,对应的Configuration.xml的设置如下:

   ```
   <!--每个priorbox层的step-->
   <PriorBoxStepWidth1>8</PriorBoxStepWidth1><!--第一个priorbox层的step的width-->
   <PriorBoxStepWidth2>16</PriorBoxStepWidth2>
   <PriorBoxStepWidth3>32</PriorBoxStepWidth3>
   
   <PriorBoxStepHeight1>8</PriorBoxStepHeight1><!--第一个priorbox层的step的height-->
   <PriorBoxStepHeight2>16</PriorBoxStepHeight2>
   <PriorBoxStepHeight3>32</PriorBoxStepHeight3>
   ```

6. **设置DetectionOutput层的参数**

   由于本示例模型是一个人脸检测模型,所以只有两类目标(背景和人脸),所以ClassNumber为2,DetectionOutput层的其他参数可以根据实际情况做微调,本示例中采用如下设置:

   ```
   <TopK>400</TopK>
   <KeepTopK>200</KeepTopK>
   <NMSThreshold>0.3</NMSThreshold>
   <ConfidenceThreshold>0.9</ConfidenceThreshold>
   ```



## 预处理

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

1. 减去均值,RetinaFace训练的时候对图像做了减均值的操作(train.py文件中的第38行),注意均值的顺序是BGR顺序。
2. 转换数据排布为NCHW



本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作:

```
ErrorCode DetectorRetinaFace::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{
	...

    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob, // 输出数据
                    scale, // 1
                    inputSize, // SSD输入大小,本示例为640x480
                    meanValue,// (104,117,123)
                    swapRB, // false
                    false);
    
    ...
 }
```



## 推理

模型转换成功并且设置好检测器参数之后就可以执行推理了。

```
ErrorCode DetectorRetinaFace::Detect(const cv::Mat &srcImage,std::vector<ResultOfDetection> &resultsOfDetection)
{

    ...
 
    // 输入数据
    migraphx::parameter_map inputData;
    inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};

    // 推理
    std::vector<migraphx::argument> inferenceResults=net.eval(inputData);
    vector<vector<float>> regressions;
    vector<vector<float>> classifications;
    for(int i=0;i<ssdParameter.numberOfPriorBoxLayer;++i) // 执行Permute操作
    {
        int numberOfPriorBox=ssdParameter.detectInputChn[i]/(4*(ssdParameter.priorBoxHeight[i] * ssdParameter.priorBoxWidth[i]));

        // BboxHead
        std::vector<float> regression;
        migraphx::argument result0  = inferenceResults[2*i]; 
        result0.visit([&](auto output) { regression.assign(output.begin(), output.end()); });
        regression=PermuteLayer(regression,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*4);
        regressions.push_back(regression);
        
        // ClassHead
        std::vector<float> classification;
        migraphx::argument result1  = inferenceResults[2*i+1]; 
        result1.visit([&](auto output) { classification.assign(output.begin(), output.end()); });
        classification=PermuteLayer(classification,ssdParameter.priorBoxWidth[i],ssdParameter.priorBoxHeight[i],numberOfPriorBox*ssdParameter.classNum);
        classifications.push_back(classification);
    }

    // 对推理结果进行处理,得到最后SSD检测的结果
    GetResult(classifications,regressions,resultsOfDetection);

    // 转换到原图坐标
    for(int i=0;i<resultsOfDetection.size();++i)
    {
        float ratioOfWidth=(1.0*srcImage.cols)/inputSize.width;
        float ratioOfHeight=(1.0*srcImage.rows)/inputSize.height;

        resultsOfDetection[i].boundingBox.x*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.width*=ratioOfWidth;
        resultsOfDetection[i].boundingBox.y*=ratioOfHeight;
        resultsOfDetection[i].boundingBox.height*=ratioOfHeight;
    }

    // 按照置信度排序
    sort(resultsOfDetection.begin(), resultsOfDetection.end(),CompareConfidence);

    return SUCCESS;

}
```

1. net.eval(inputData)返回推理结果,顺序与onnx输出保持一致,可以通过netron查看输出节点顺序,其中inferenceResults[2 * i]表示每个检测层的BboxHead的输出,inferenceResults [2 * i + 1]表示每个检测层的ClassHead的输出。

1. 经过PermuteLayer处理之后的所有检测层数据通过GetResult()得到最后的输出结果,注意这里的输出结果还不是最后的检测结果,最后需要转换到原图坐标才能够得到最终的检测结果。



## 运行示例

根据samples工程中的README.md构建成功C++ samples后,在build目录下输入如下命令运行该示例:

```
./MIGraphX_Samples 2
```

会在当前目录生成检测结果图像Result.jpg

![image-20221215164140724](../Images/RetinaFace_03.png)