# 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上运行。 ![image-20221214191602193](../Images/SSD_01.png) 图中黄色部分为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文件的输出顺序如下: ![image-20221214202259180](../Images/SSD_02.png) 所以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