# YOLOV3检测器

## 模型简介

YOLOV3是由Joseph Redmon和Ali Farhadi在《YOLOv3: An Incremental Improvement》论文中提出的单阶段检测模型，算法首先通过特征提取网络对输入提取特征，backbone部分由YOLOV2时期的Darknet19进化至Darknet53加深了网络层数，引入了Resnet中的跨层加和操作；然后结合不同卷积层的特征实现多尺度训练，一共有13x13、26x26、52x52三种分辨率，分别用来预测大、中、小的物体；每种分辨率的特征图将输入图像分成不同数量的格子，每个格子预测B个bounding box，每个bounding box预测内容包括: Location(x, y, w, h)、Confidence Score和C个类别的概率，因此YOLOv3输出层的channel数为B*(5 + C)。YOLOv3的loss函数也有三部分组成：Location误差，Confidence误差和分类误差。

<img src="../Images/YOLOV3_02.jpg" alt="YOLOV3_02" style="zoom:;" />

本示例采用如下的开源实现：https://github.com/ultralytics/yolov3, 作者在V9.6.0版本中提供多种不同的YOLOV3预训练模型，其中包括yolov3、yolov3-fixed、yolov3-spp、yolov3-tiny四个版本。本示例选择yolov3-tiny.pt预训练模型进行构建MIGraphX推理，下载YOLOV3的预训练模型yolov3-tiny.pt保存在Pytorch_YOLOV3工程的weights目录。

## 模型转换

官方提供的YOLOV3源码中包含导出onnx模型的程序，通过下面的步骤可以将yolov3-tiny.pt转换成onnx格式：

```
# 进入Pytorch_YOLOV3工程根目录
cd <path_to_Pytorch_YOLOV3>

# 环境配置，torch、torchvision手动安装
pip install -r requirements.txt

# 导出onnx模型
python export.py --weights yolov3.pt --imgsz 416 416 --include onnx
```

注意：官方源码提供的模型转换的程序中包含更多的功能，例如动态shape模型的导出，可根据需要进行添加相关参数。

## 检测器参数设置

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

- ModelPath：yolov3模型存放路径
- ClassNameFile：coco数据集类别文件存放路径
- UseFP16：是否使用FP16推理模式
- NumberOfClasses：检测类别数量
- ConfidenceThreshold：置信度阈值，用于判断anchor内的物体是否为正样本
- NMSThreshold：非极大值抑制阈值，用于消除重复框
- ObjectThreshold：用于判断anchor内部是否有物体

```
<ModelPath>"../Resource/Models/Detector/YOLOV3/yolov3-tiny.onnx"</ModelPath>
<ClassNameFile>"../Resource/Models/Detector/YOLOV3/coco.names"</ClassNameFile>
<UseFP16>0</UseFP16><!--是否使用FP16-->
<NumberOfClasses>80</NumberOfClasses><!--类别数(不包括背景类)，COCO:80,VOC:20-->
<ConfidenceThreshold>0.2</ConfidenceThreshold>
<NMSThreshold>0.4</NMSThreshold>
<ObjectThreshold>0.4</ObjectThreshold>
```

## 模型初始化

模型初始化首先通过parse_onnx()函数加载YOLOV3的onnx模型，并可以通过program的get_parameter_shapes()函数获取网络的输入属性。完成模型加载之后需要使用compile()方法编译模型，编译模式使用migraphx::gpu::target{}设为GPU模式，编译过程主要基于MIGraphX IR完成各种优化。同时如果需要使用低精度量化进行推理，可以使用quantize_fp16()函数实现。

```
ErrorCode DetectorYOLOV3::Initialize(InitializationParameterOfDetector initializationParameterOfDetector)
{
    ...
    
    //模型加载
    net = migraphx::parse_onnx(modelPath);
    LOG_INFO(logFile,"succeed to load model: %s\n",GetFileName(modelPath).c_str());

    // 获取模型输入属性
    std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
    inputName=inputAttribute.first;
    inputShape=inputAttribute.second;
    inputSize=cv::Size(inputShape.lens()[3],inputShape.lens()[2]);// NCHW

    // 设置模型为GPU模式
    migraphx::target gpuTarget = migraphx::gpu::target{};

    // 量化    
    if(useFP16)
    {
        migraphx::quantize_fp16(net);
    }

    // 编译模型
    migraphx::compile_options options;
    options.device_id=0; // 设置GPU设备，默认为0号设备(>=1.2版本中支持)
    options.offload_copy=true; // 设置offload_copy
    net.compile(gpuTarget,options);
    LOG_INFO(logFile,"succeed to compile model: %s\n",GetFileName(modelPath).c_str());

    ...
}
```

## 模型推理

### 预处理

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

- 转换数据排布为NCHW
- 归一化[0.0, 1.0]
- 将输入数据的尺寸变换到YOLOV3输入大小（1，3，416，416）

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

    // 预处理并转换为NCHW
    cv::Mat inputBlob;
    blobFromImage(srcImage,   // 输入数据
                    inputBlob,  // 输出数据
                    1 / 255.0,  //归一化
                    inputSize,  //YOLOV3输入尺寸，本示例为416x416
                    Scalar(0, 0, 0),  //未减去均值
                    true,  //转换RB通道
                    false);
                    
    ...
}
```

### 前向推理

完成图像预处理以及YOLOV3目标检测相关参数设置之后开始执行推理，利用migraphx推理计算得到YOLOV3模型的输出数据。

```
ErrorCode DetectorYOLOV3::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);

    // 获取推理结果
    std::vector<cv::Mat> outs;
    migraphx::argument result = inferenceResults[0]; 

    // 转换为cv::Mat
    migraphx::shape outputShape = result.get_shape();
    int shape[]={outputShape.lens()[0],outputShape.lens()[1],outputShape.lens()[2]};
    cv::Mat out(3,shape,CV_32F);
    memcpy(out.data,result.data(),sizeof(float)*outputShape.elements());
    outs.push_back(out);
    
    ...
}
```

YOLOV3的MIGraphX推理结果inferenceResults是一个std::vector< migraphx::argument >类型，YOLOV3的onnx模型包含一个输出，所以result等于inferenceResults[0]，result包含三个维度：outputShape.lens()[0]=1表示batch信息，outputShape.lens()[1]=10647表示生成anchor数量，outputShape.lens()[2]=85表示对每个anchor的预测信息。同时可将85拆分为4+1+80，前4个参数用于判断每一个特征点的回归参数，回归参数调整后可以获得预测框，第5个参数用于判断每一个特征点是否包含物体，最后80个参数用于判断每一个特征点所包含的物体种类。获取上述信息之后进行anchors筛选，筛选过程分为两个步骤：

- 第一步根据objectThreshold阈值进行筛选，大于该阈值则判断当前anchor内包含物体，小于该阈值则判断无物体；

- 第二步根据confidenceThreshold阈值进行筛选，当满足第一步阈值anchor的最大置信度得分maxClassScore大于该阈值，则进一步获取当前anchor的坐标信息和预测物体类别信息，小于该阈值则不做处理。

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{
    ...
    
    //获取先验框的个数numProposal=10647
    int numProposal = outs[0].size[1];
    //获取每个anchor的预测信息数量numOut=85
    int numOut = outs[0].size[2];
    //变换输出的维度
    outs[0] = outs[0].reshape(0, numProposal);

    //生成先验框
    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;

    //计算cx,cy,w,h,box_sore,class_sore
    int n = 0, rowInd = 0;
    float* pdata = (float*)outs[0].data;
    for (n = 0; n < numProposal; n++)
    {
        //获取当前anchor是否包含物体的概率值
        float boxScores = pdata[4];
        //第一次筛选，判断anchor内是否包含物体
        if (boxScores > yolov3Parameter.objectThreshold)
        {
            //获取每个anchor内部预测的80个类别概率信息
            cv::Mat scores = outs[0].row(rowInd).colRange(5, numOut);
            cv::Point classIdPoint;
            double maxClassScore;
            /获取80个类别中最大概率值和对应的类别ID
            cv::minMaxLoc(scores, 0, &maxClassScore, 0, &classIdPoint);
            maxClassScore *= boxScores;
            //第二次筛选，判断当前anchor的最大置信度得分是否满足阈值
            if (maxClassScore > yolov3Parameter.confidenceThreshold)
            {
                const int classIdx = classIdPoint.x;
                //将每个anchor坐标按缩放比例映射到原图
                float cx = pdata[0] * ratiow;
                float cy = pdata[1] * ratioh;
                float w = pdata[2] * ratiow;
                float h = pdata[3] * ratioh;
                //获取anchor的左上角坐标
                int left = int(cx - 0.5 * w);
                int top = int(cy - 0.5 * h);

                confidences.push_back((float)maxClassScore);
                boxes.push_back(cv::Rect(left, top, (int)(w), (int)(h)));
                classIds.push_back(classIdx);
            }
        }
        rowInd++;
        pdata += numOut;
    }

    ...
}
```

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

```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector<ResultOfDetection> &resultsOfDetection)
{    
    ...
    //执行non maximum suppression消除冗余重叠boxes
    std::vector<int> indices;
    dnn::NMSBoxes(boxes, confidences, yolov3Parameter.confidenceThreshold, yolov3Parameter.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];、
        //保存每个最终预测anchor的坐标值、置信度分数、类别ID
        ResultOfDetection result;
        result.boundingBox=box;
        result.confidence=confidence;// confidence
        result.classID=classID; // label
        result.className=className;
        resultsOfDetection.push_back(result);
    }
    ...
}
```

## 运行示例

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

```
./MIGraphX_Samples 4
```

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

![YOLOV3_01](../Images/YOLOV3_01.jpg)
