# RetinaFace人脸检测器



RetinaFace是目前被广泛使用的一种人脸检测器模型，本示例主要说明了如何在MIGraphX中使用RetinaFace人脸检测器。



## 模型简介

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

<img src="./Image/RetinaFace_01.png" style="zoom:60%;" align=center>



## 检测器参数设置

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

-  **设置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，

   <img src="./Image/RetinaFace_03.png" style="zoom:70%;" align=center>
   
   
   所以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>
   ```
   
-  **设置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>
   ```

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

-  **设置每个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>
   ```

-  **设置DetectionOutput层的参数**

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

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



## 预处理

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

- 减去均值，RetinaFace训练的时候对图像做了减均值的操作(train.py文件中的第38行)，注意均值的顺序是BGR顺序。
- 转换数据排布为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)
{

    ...
 
    // 输入数据
    std::unordered_map<std::string, migraphx::shape> 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;

}
```

-  net.eval(inputData)返回推理结果，顺序与onnx输出保持一致，可以通过netron查看输出节点顺序，其中inferenceResults[2 * i]表示每个检测层的BboxHead的输出，inferenceResults [2 * i + 1]表示每个检测层的ClassHead的输出。
-  经过PermuteLayer处理之后的所有检测层数据通过GetResult()得到最后的输出结果，注意这里的输出结果还不是最后的检测结果，最后需要转换到原图坐标才能够得到最终的检测结果。

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

```
...
// 推理
std::vector<std::string> outputNames = {"467","470","468"};
std::vector<migraphx::argument> results = net.eval(inputData, outputNames);
...
```