# SSD检测器
## 模型简介
SSD检测器示例使用了经典的SSD算法(https://arxiv.org/abs/1512.02325),原论文作者使用Caffe框架实现,本示例采用的是于仕琪老师开源的YuFaceDetectNet人脸检测模型,最初也是采用Caffe框架实现的,从下面的commit对应的工程中可以下载到Caffe模型:https://github.com/ShiqiYu/libfacedetection/commit/54b8e036b299b4763afa6a74af2502a8b13eb0ad,模型在libfacedetection/models/caffe目录下,该目录下同时提供了训练和部署的模型结构,本示例采用了yufacedetectnet-open-v2模型。
## 模型转换
由于MIGraphX不支持YuFaceDetectNet模型中的Permute,PriorBox和DetectionOutput这几个层,所以需要对模型做一些修改,基本的思路就是让MIGraphX不支持的层在CPU上运行。

图中黄色部分为MIGraphX不支持的层,导出onnx模型的时候需要删除这些层,将这些层都放到CPU上执行。 修改Resource/Models/Detector/SSD/yufacedetectnet-open-v2.prototxt文件的时候,直接删除不支持的层即可,修改后的文件为Resource/Models/Detector/SSD/yufacedetectnet-open-v2_onnx.prototxt文件,可以使用比较工具查看修改的部分,使用yufacedetectnet-open-v2_onnx.prototxt和yufacedetectnet-open-v2.caffemodel通过caffe-onnx(https://github.com/htshinichi/caffe-onnx)这个转换工具就可以将Caffe模型转换为onnx模型了。
注意:使用Caffe训练SSD模型的时候,需要去掉Normalize层,否则使用caffe-onnx工具转换模型的时候会失败,本示例中直接将yufacedetectnet-open-v2.prototxt中的Normalize层去掉了。
## SSD参数设置
在运行模型前,需要设置SSD模型的参数,Resource/Configuration.xml文件中的DetectorSSD节点对应了本示例使用的YuFaceDetectNet检测器的参数,下面看一下是如何根据yufacedetectnet-open-v2.prototxt设置Configuration.xml中的参数的,可以使用netscope (http://ethereon.github.io/netscope/#/editor ) 可视化.prototxt文件,方便观察模型结构,在yufacedetectnet-open-v2.prototxt中一共有4个priorbox层,分别为conv3_3_norm_mbox_priorbox,conv4_3_norm_mbox_priorbox,conv5_3_norm_mbox_priorbox,conv6_3_norm_mbox_priorbox,以conv3_3_norm_mbox_priorbox为例,其prototxt文件中的代码如下:
```
layer {
name: "conv3_3_norm_mbox_priorbox"
type: "PriorBox"
bottom: "conv3_3_norm"
bottom: "data"
top: "conv3_3_norm_mbox_priorbox"
prior_box_param {
min_size: 10.0
min_size: 16.0
min_size: 24.0
clip: false
variance: 0.10000000149
variance: 0.10000000149
variance: 0.20000000298
variance: 0.20000000298
step: 8.0
offset: 0.5
}
}
```
一共有3个min_size:10,16,24,同时clip为fase且step为8,该层没有设置flip参数,所以采用默认值true(注:如果该层只有一个宽高比为1的anchor,则flip参数可以设置为false),由于该priorbox层没有设置其他宽高比,只包含一个宽高比为1的anchor,所以Configuration.xml中不需要设置宽高比,所以配置文件中对应的参数为:
```
10
16
24
0
0
8
8
```
如果你需要添加其他宽高比的anchor,在设置priorbox层参数的时候需要注意:Configuration.xml中AspectRatio参数设置的时候不需要包含1,因为程序中默认已经添加1了,同时需要忽略flip参数,比如现在有4个priorbox层,每一层的宽高比设置为0.3333和0.25且flip为true,则每一层只需要写0.3333和0.25即可,xml代码如下:
```
0.3333
0.25
0.3333
0.25
0.3333
0.25
0.3333
0.25
```
其他priorbox层参数的设置与conv3_3_norm_mbox_priorbox类似,这里需要注意:**Configuration.xml中priorbox层的顺序要与onnx文件中的输出节点顺序保持一致**,通过netron (https://netron.app/) 查看到onnx文件的输出顺序如下:

所以Configuration.xml中priorbox层的顺序为conv3_3_norm_mbox_priorbox,conv4_3_norm_mbox_priorbox,conv5_3_norm_mbox_priorbox,conv6_3_norm_mbox_priorbox,所以Configuration.xml中minisize参数设置如下:
```
10
16
24
32
48
64
96
128
192
256
```
设置好priorbox参数后,还需要设置DetectionOutput层的参数,由于本示例模型是一个人脸检测模型,所以只有两类目标(背景和人脸),所以ClassNumber为2,DetectionOutput层的其他参数可以根据实际情况做微调,本示例中采用如下设置:
```
400
200
0.3
0.9
```
## 预处理
在将数据输入到模型之前,需要对图像做如下预处理操作:
1. 减去均值,本示例使用的模型不需要减均值
2. 转换数据排布为NCHW
本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作:
```
ErrorCode DetectorSSD::Detect(const cv::Mat &srcImage,std::vector &resultsOfDetection)
{
...
// 预处理并转换为NCHW
cv::Mat inputBlob;
blobFromImage(srcImage, // 输入数据
inputBlob, // 输出数据
scale, // 1
inputSize, // SSD输入大小,本示例为640x480
meanValue,// 本示例不需要减均值,这里设置为0
swapRB, // false
false);
...
}
```
## 推理
模型转换成功并且设置好SSD参数之后就可以执行推理了,对于MIGraphX不支持的SSD层,需要在CPU上实现,示例代码Src/Detector/DetectorSSD.h中的PermuteLayer(),PriorBoxLayer(),DetectionOutputLayer()分别实现了对应的层。
```
ErrorCode DetectorSSD::Detect(const cv::Mat &srcImage,std::vector &resultsOfDetection)
{
...
// 输入数据
migraphx::parameter_map inputData;
inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
// 推理
std::vector inferenceResults=net.eval(inputData);
vector> regressions;
vector> classifications;
for(int i=0;i 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);
// 分类
std::vector 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