# YOLOV3检测器
该示例是基于YOLOV3的onnx模型在DCU平台构建的C++推理示例,介绍了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误差和分类误差。
## 模型初始化
Initialize函数实现模型加载、解析和编译等功能,调用推理框架parse_onnx()函数对YOLOV3模型进行加载解析,从而获取模型的输入属性,为后续图像预处理准备必要的尺寸参数。设置GPU推理模式并调用compile()方法编译模型,编译过程主要完成了各种优化。
```
ErrorCode DetectorYOLOV3::Initialize(InitializationParameterOfDetector initializationParameterOfDetector)
{
...
//模型加载
net = migraphx::parse_onnx(modelPath);
LOG_INFO(stdout,"succeed to load model: %s\n",GetFileName(modelPath).c_str());
// 获取模型输入/输出节点信息
std::cout<<"inputs:"< inputs=net.get_inputs();
for(auto i:inputs)
{
std::cout< outputs=net.get_outputs();
for(auto i:outputs)
{
std::cout<first;
inputShape=inputs.begin()->second;
int N=inputShape.lens()[0];
int C=inputShape.lens()[1];
int H=inputShape.lens()[2];
int W=inputShape.lens()[3];
inputSize=cv::Size(W,H);
// 设置模型为GPU模式
migraphx::target gpuTarget = migraphx::gpu::target{};
// 量化
if(useFP16)
{
migraphx::quantize_fp16(net);
}
// 编译模型
migraphx::compile_options options;
options.device_id=0; // 设置GPU设备,默认为0号设备
options.offload_copy=true; // 设置offload_copy
net.compile(gpuTarget,options);
LOG_INFO(stdout,"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 &resultsOfDetection)
{
...
// 预处理并转换为NCHW
cv::Mat inputBlob;
blobFromImage(srcImage, // 输入数据
inputBlob, // 输出数据
1 / 255.0, //归一化
inputSize, //YOLOV3输入尺寸,本示例为416x416
Scalar(0, 0, 0), //未减去均值
true, //转换RB通道
false);
...
}
```
## 推理
完成图像预处理以及YOLOV3目标检测相关参数设置之后开始执行推理,利用migraphx推理计算得到YOLOV3模型的输出数据。由于导出的onnx模型只包含一个输出,所以result等于inferenceResults[0],为了便于后处理将result由std::vector< migraphx::argument >类型转换为cv::Mat类型。result的维度为{1,10647,85},其中第二维度outputShape.lens()[1]的数值与输入图像的尺寸有关,表示在当前图像生成anchor的数量,第三维度outputShape.lens()[2]表示每个anchor的预测信息,可将85拆分为4+1+80,前4个参数用于判断每一个特征点的回归参数,回归参数调整后可以获得预测框,第5个参数用于判断每一个特征点是否包含物体,本示例使用的预训练模型基于COCO数据集训练,所以最后80个参数表示物体类别个数。
```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector &resultsOfDetection)
{
...
// 创建输入数据
std::unordered_map inputData;
inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
// 推理
std::vector inferenceResults = net.eval(inputData);
// 获取推理结果
std::vector 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);
...
}
```
另外,如果想要指定输出节点,可以在eval()方法中通过提供outputNames参数来实现:
```
...
// 推理
std::vector outputNames = {"187","121","output"}
std::vector inferenceResults = net.eval(inputData, outputNames);
...
```
如果没有指定outputName参数,则默认输出所有输出节点,此时输出节点的顺序与ONNX中输出节点顺序保持一致,可以通过netron查看ONNX文件的输出节点的顺序。
获取上述信息之后进行anchors筛选,筛选过程分为两个步骤:
- 第一步根据objectThreshold阈值进行筛选,大于该阈值则判断当前anchor内包含物体,小于该阈值则判断无物体;
- 第二步根据confidenceThreshold阈值进行筛选,当满足第一步阈值anchor的最大置信度得分maxClassScore大于该阈值,则进一步获取当前anchor的坐标信息和预测物体类别信息,小于该阈值则不做处理。
```
ErrorCode DetectorYOLOV3::Detect(const cv::Mat &srcImage, std::vector &resultsOfDetection)
{
...
//获取先验框的个数numProposal=10647
int numProposal = outs[0].size[1];
int numOut = outs[0].size[2];
outs[0] = outs[0].reshape(0, numProposal);
//生成先验框
std::vector confidences;
std::vector boxes;
std::vector 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)
{
cv::Mat scores = outs[0].row(rowInd).colRange(5, numOut);
cv::Point classIdPoint;
double maxClassScore;
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 &resultsOfDetection)
{
...
//执行non maximum suppression消除冗余重叠boxes
std::vector 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);
}
...
}
```