# RetinaFace人脸检测器 ## 模型简介 RetinaFace是一个经典的人脸检测模型(https://arxiv.org/abs/1905.00641),采用了SSD架构。 ![image-20221215140647406](../Images/RetinaFace_01.png) 本示例采用了如下的开源实现:https://github.com/biubug6/Pytorch_Retinaface,作者提供了restnet50 和mobilenet0.25两个预训练模型,本示例使用了mobilenet0.25预训练模型,将mobilenet0.25预训练模型下载下来后,保存到Pytorch_Retinaface工程的weights目录。 ## 模型转换 在将mobilenet0.25预训练模型转换为onnx文件的时候,本示例需要对作者提供的python代码做如下改变: ### 修改models/retinaface.py 1. **将ClassHead类修改为如下实现** ``` class ClassHead(nn.Module): def __init__(self,inchannels=512,num_anchors=3): super(ClassHead,self).__init__() self.num_anchors = num_anchors self.conv1x1 = nn.Conv2d(inchannels,self.num_anchors*2,kernel_size=(1,1),stride=1,padding=0) def forward(self,x): out = self.conv1x1(x) return out ``` 由于本示例的C++推理代码已经实现了permute操作,所以这里需要去掉out.permute(0,2,3,1).contiguous()。 2. **将BboxHead类修改为如下实现** ``` class BboxHead(nn.Module): def __init__(self,inchannels=512,num_anchors=3): super(BboxHead,self).__init__() self.conv1x1 = nn.Conv2d(inchannels,num_anchors*4,kernel_size=(1,1),stride=1,padding=0) def forward(self,x): out = self.conv1x1(x) return out ``` 与ClassHead一样,需要去掉permute操作。 3. **将RetinaFace类的forward修改为如下实现** ``` def forward(self,inputs): out = self.body(inputs) # FPN fpn = self.fpn(out) # SSH feature1 = self.ssh1(fpn[0]) feature2 = self.ssh2(fpn[1]) feature3 = self.ssh3(fpn[2]) features = [feature1, feature2, feature3] bbox_regressions = [self.BboxHead[i](feature) for i, feature in enumerate(features)] classifications = [self.ClassHead[i](feature) for i, feature in enumerate(features)] output=(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2]) return output ``` 本示例去掉了landmark检测功能,所以需要去掉forward中的landmark部分,bbox_regressions和classifications需要删除torch.cat操作,同时需要修改output为(bbox_regressions[0],classifications[0],bbox_regressions[1],classifications[1],bbox_regressions[2],classifications[2])。 ### 修改data/config.py 将cfg_mnet中的'pretrain': True,修改为'pretrain': False, ### 修改convert_to_onnx.py 导出onnx模型的时候,需要修改原来的output_names,可以直接删除torch.onnx._export()的output_names参数或者手动指定每个输出节点的名字,如果直接删除了output_names参数,则会生成一个随机名,本示例直接删除了output_names参数,同时本示例修改了onnx文件名output_onnx,修改后的main函数如下: ``` if __name__ == '__main__': torch.set_grad_enabled(False) cfg = None if args.network == "mobile0.25": cfg = cfg_mnet elif args.network == "resnet50": cfg = cfg_re50 # net and model net = RetinaFace(cfg=cfg, phase = 'test') net = load_model(net, args.trained_model, args.cpu) net.eval() print('Finished loading model!') print(net) device = torch.device("cpu" if args.cpu else "cuda") net = net.to(device) # ------------------------ export ----------------------------- output_onnx = 'mobilenet0.25_Final.onnx' print("==> Exporting model to ONNX format at '{}'".format(output_onnx)) input_names = ["input0"] output_names = ["output0"] inputs = torch.randn(1, 3, args.long_side, args.long_side).to(device) torch_out = torch.onnx._export(net, inputs, output_onnx, export_params=True, verbose=False, input_names=input_names) ``` 注意:如果需要修改模型的输入大小,可以修改args.long_side参数,默认为640x640。 完成上述修改后,执行python convert_to_onnx.py命令就可以实现模型转换了,转换成功后会在当前目录生成mobilenet0.25_Final.onnx文件,下面就可以进行推理了。本示例将修改好的工程保存到了samples工程中的Resource/Models/Detector/RetinaFace目录中,在Pytorch_Retinaface目录中执行python convert_to_onnx.py命令可以直接生成onnx文件。 ## 检测器参数设置 samples工程中的Resource/Configuration.xml文件的DetectorRetinaFace节点表示RetinaFace检测器的参数,这些参数是根据Pytorch_Retinaface工程中的data/config.py文件中的cfg_mnet来设置的,下面我们看一下是如何通过cfg_mnet来设置的。 2. **设置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, ![image-20221215153957174](../Images/RetinaFace_02.png) 所以Configuration.xml配置文件中的参数设置如下: ``` 3 16 32 64 128 256 512 ``` 3. **设置Flip和Clip** cfg_mnet中的clip为False,所以Configuration.xml中对应的参数设置为0即可,由于只有一个宽高比为1的anchor,所以Flip设置为0。 ``` 0 0 0 0 0 0 ``` 4. **设置anchor的宽高比** 由于RetinaFace只包含宽高比为1的anchor,所以这里不需要设置宽高比。 5. **设置每个priorbox层的步长** cfg_mnet中的steps表示每个priorbox层的步长,所以三个priorbox的步长依次为8,16,32,对应的Configuration.xml的设置如下: ``` 8 16 32 8 16 32 ``` 6. **设置DetectionOutput层的参数** 由于本示例模型是一个人脸检测模型,所以只有两类目标(背景和人脸),所以ClassNumber为2,DetectionOutput层的其他参数可以根据实际情况做微调,本示例中采用如下设置: ``` 400 200 0.3 0.9 ``` ## 预处理 在将数据输入到模型之前,需要对图像做如下预处理操作: 1. 减去均值,RetinaFace训练的时候对图像做了减均值的操作(train.py文件中的第38行),注意均值的顺序是BGR顺序。 2. 转换数据排布为NCHW 本示例代码采用了OpenCV的cv::dnn::blobFromImage()函数实现了预处理操作: ``` ErrorCode DetectorRetinaFace::Detect(const cv::Mat &srcImage,std::vector &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 &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); // ClassHead 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