东方耀AI技术分享

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
热搜: 活动 交友 discuz
查看: 3717|回复: 1
打印 上一主题 下一主题

[课堂笔记] 人脸检测业务项目实战(基于SSD算法改进)

[复制链接]

1365

主题

1856

帖子

1万

积分

管理员

Rank: 10Rank: 10Rank: 10

积分
14435
QQ
跳转到指定楼层
楼主
发表于 2019-9-4 15:55:55 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式


人脸检测业务场景综述
判断是否存在人脸,如果存在人脸则定位到人脸的位置
标准的目标检测问题(针对人脸目标)
1、姿态和表情的变化
2、不同人的外观差异(是否戴眼镜,是否戴口罩 疫情严重)
3、光照,遮挡的影响
4、不同视角
5、不同大小、位置

人脸标注方法:矩形标注

人脸检测公开数据集非常多:FDDB LFW WiderFace MegaFace 等


选择数据集资源:WIDER FACE(难度是比较大的)
1、香港中文大学 Yang Shuo, Luo Ping,Loy, Chen Change,Tang Xiaoou收集
2、包含32203个图像和393703个人脸图像
3、在尺度、姿势、装扮、光照等方面表现出了大的变化
4、基于61个事件类别(场景)组织的,对于每一个事件类别,选取其中的40%作为训练集,10%用于交叉验证50%作为测试集
5、下载链接:http://shuoyang1213.me/WIDERFACE/


人脸数据采集可能遇到的问题:
1、不同性别分布,男性、女性。
2、不同年龄分布,儿童、少年、中年、老年。
3、不同人种分布,黑人、白人、黄种人。
4、不同脸型分布,人脸、猪脸、猴脸。
5、人脸没有正对摄像头,角度有倾斜,左右倾斜、上下倾斜。
6、翻拍的人脸照片,清晰照片、不清晰照片。
7、摄像头内包含单张人脸、多张人脸。
8、测试所处的环境:光线正常、过亮、过暗、暖光、冷光、白平衡等
9、不同场景:室内、室外、车站、超市等


人脸验证时,可能会存在的非法行为:
1、长相相似度很高的非本人的照片
2、双胞胎照片
3、整容过的照片
4、软件合成的虚拟人脸
5、基于证件照PS的照片


项目开发环境:
OS:Ubuntu18.04
conda 4.7.10虚拟环境
python:2.7.15
caffe-ssd源码
IDEycharm

WiderFace数据转换为VOC格式的数据脚本:
  1. # -*- coding: utf-8 -*-
  2. __author__ = u'东方耀 微信:dfy_88888'
  3. __date__ = '2019/7/15 下午3:23'
  4. __product__ = 'PyCharm'
  5. __filename__ = 'widerface2voc'

  6. # import os, cv2, sys, shutil
  7. import cv2
  8. import shutil
  9. from xml.dom.minidom import Document

  10. root_dir = '/home/dfy888/DataSets/WIDER Face DataSet'
  11. root_dir_voc = '/home/dfy888/DataSets/widerface_voc'


  12. def writexml(filename, saveimg, bboxes, xmlpath):
  13.     """
  14.     写成voc格式通用的xml文件
  15.     :param filename: 图片的路径
  16.     :param saveimg: 图片对象 cv2
  17.     :param bboxes: 多个人脸框集合
  18.     :param xmlpath: xml文件路径
  19.     :return:
  20.     """
  21.     doc = Document()
  22.     # 根节点
  23.     annotation = doc.createElement('annotation')
  24.     doc.appendChild(annotation)

  25.     folder = doc.createElement('folder')
  26.     # 注意:widerface_voc voc格式数据的文件夹名字
  27.     folder_name = doc.createTextNode('widerface_voc')
  28.     folder.appendChild(folder_name)
  29.     annotation.appendChild(folder)

  30.     filenamenode = doc.createElement('filename')
  31.     filename_name = doc.createTextNode(filename)
  32.     filenamenode.appendChild(filename_name)
  33.     annotation.appendChild(filenamenode)

  34.     source = doc.createElement('source')
  35.     annotation.appendChild(source)

  36.     database = doc.createElement('database')
  37.     database.appendChild(doc.createTextNode('wider face Database'))
  38.     source.appendChild(database)

  39.     annotation_s = doc.createElement('annotation')
  40.     annotation_s.appendChild(doc.createTextNode('PASCAL VOC2007'))
  41.     source.appendChild(annotation_s)

  42.     image = doc.createElement('image')
  43.     image.appendChild(doc.createTextNode('flickr'))
  44.     source.appendChild(image)

  45.     flickrid = doc.createElement('flickrid')
  46.     flickrid.appendChild(doc.createTextNode('-1'))
  47.     source.appendChild(flickrid)

  48.     owner = doc.createElement('owner')
  49.     annotation.appendChild(owner)

  50.     flickrid_o = doc.createElement('flickrid')
  51.     flickrid_o.appendChild(doc.createTextNode('dfy_88888'))
  52.     owner.appendChild(flickrid_o)

  53.     name_o = doc.createElement('name')
  54.     name_o.appendChild(doc.createTextNode('dfy_88888'))
  55.     owner.appendChild(name_o)

  56.     size = doc.createElement('size')
  57.     annotation.appendChild(size)

  58.     width = doc.createElement('width')
  59.     width.appendChild(doc.createTextNode(str(saveimg.shape[1])))
  60.     height = doc.createElement('height')
  61.     height.appendChild(doc.createTextNode(str(saveimg.shape[0])))
  62.     depth = doc.createElement('depth')
  63.     depth.appendChild(doc.createTextNode(str(saveimg.shape[2])))
  64.     size.appendChild(width)
  65.     size.appendChild(height)
  66.     size.appendChild(depth)

  67.     segmented = doc.createElement('segmented')
  68.     segmented.appendChild(doc.createTextNode('0'))
  69.     annotation.appendChild(segmented)

  70.     for i in range(len(bboxes)):
  71.         # bbox 四维向量: [左上角坐标x y 宽高 w h]
  72.         bbox = bboxes[i]
  73.         objects = doc.createElement('object')
  74.         annotation.appendChild(objects)

  75.         object_name = doc.createElement('name')
  76.         # 只有人脸
  77.         object_name.appendChild(doc.createTextNode('face'))
  78.         objects.appendChild(object_name)

  79.         pose = doc.createElement('pose')
  80.         pose.appendChild(doc.createTextNode('Unspecified'))
  81.         objects.appendChild(pose)

  82.         truncated = doc.createElement('truncated')
  83.         truncated.appendChild(doc.createTextNode('1'))
  84.         objects.appendChild(truncated)

  85.         difficult = doc.createElement('difficult')
  86.         difficult.appendChild(doc.createTextNode('0'))
  87.         objects.appendChild(difficult)

  88.         bndbox = doc.createElement('bndbox')
  89.         objects.appendChild(bndbox)
  90.         # xmin ymin 就是标记框 左上角的坐标
  91.         xmin = doc.createElement('xmin')
  92.         xmin.appendChild(doc.createTextNode(str(bbox[0])))
  93.         bndbox.appendChild(xmin)
  94.         ymin = doc.createElement('ymin')
  95.         ymin.appendChild(doc.createTextNode(str(bbox[1])))
  96.         bndbox.appendChild(ymin)
  97.         # xmax ymax 就是标记框 右下角的坐标
  98.         xmax = doc.createElement('xmax')
  99.         xmax.appendChild(doc.createTextNode(str(bbox[0] + bbox[2])))
  100.         bndbox.appendChild(xmax)
  101.         ymax = doc.createElement('ymax')
  102.         ymax.appendChild(doc.createTextNode(str(bbox[1] + bbox[3])))
  103.         bndbox.appendChild(ymax)

  104.     with open(xmlpath, 'w') as f:
  105.         f.write(doc.toprettyxml(indent=''))


  106. def convert_imgset(img_set_type):
  107.     """
  108.     转换数据集(WiderFace---> VOC)
  109.     :param img_set_type: train or val
  110.     :return:
  111.     """
  112.     # 对应数据集中原始图片的路径
  113.     img_dir = root_dir + '/WIDER_' + img_set_type + '/images'
  114.     # ground truth 的路径 (标注文件中)
  115.     gt_filepath = root_dir + '/wider_face_split/wider_face_' + img_set_type + '_bbx_gt.txt'

  116.     fwrite = open(root_dir_voc + '/ImageSets/Main/' + img_set_type + '.txt', 'w')

  117.     print(img_dir)
  118.     print(gt_filepath)

  119.     # 表示我们解析到了第几张图片
  120.     index = 0
  121.     no_face_index = []
  122.     with open(gt_filepath, 'r') as gt_files:
  123.         # 为了快速 只取1000个图片样本  实际可以是True
  124.         while(index < 5):
  125.             # 为什么是[: -1]? 去掉最后的空格
  126.             filename = gt_files.readline().strip()
  127.             # print('读取的filename:%s,其长度为:%d' % (filename, len(filename)))
  128.             if filename == '' or filename is None:
  129.                 break
  130.             # 图片的绝对路径
  131.             img_path = img_dir + '/' + filename
  132.             print('读取的图片绝对路径:', img_path)
  133.             img = cv2.imread(img_path)
  134.             # 可视化看看图片
  135.             # cv2.imshow('1', img)
  136.             # cv2.waitKey(0)
  137.             if not img.data:
  138.                 break

  139.             num_bbox = int(gt_files.readline())

  140.             if num_bbox == 0:
  141.                 # 还是需要读一下
  142.                 line = gt_files.readline()
  143.                 no_face_index.append(index)
  144.                 print('没有人脸框的特殊情况:', line)

  145.             bboxes = []
  146.             for i in range(num_bbox):
  147.                 # 每读取一行 就是一个人脸框 gt
  148.                 line = gt_files.readline()
  149.                 lines = line.split()
  150.                 # 前面4个值
  151.                 lines = lines[0: 4]
  152.                 # bbox 四维向量: [左上角坐标x y 宽高 w h]
  153.                 bbox = (int(lines[0]), int(lines[1]), int(lines[2]), int(lines[3]))

  154.                 # 可视化看看人脸框的矩形
  155.                 cv2.rectangle(img, (int(lines[0]), int(lines[1])),
  156.                               (int(lines[0]) + int(lines[2]), int(lines[1]) + int(lines[3])),
  157.                               color=(0, 0, 255), thickness=1)

  158.                 bboxes.append(bbox)

  159.             cv2.imshow(str(index), img)
  160.             cv2.waitKey(0)

  161.             filename = filename.replace('/', '_')
  162.             print('保存后的filename:', filename)

  163.             if len(bboxes) == 0:
  164.                 print('no face box')
  165.                 index += 1
  166.                 continue

  167.             cv2.imwrite('{}/JPEGImages/{}'.format(root_dir_voc, filename), img)

  168.             fwrite.write(filename.split('.')[0] + '\n')

  169.             xmlpath = '{}/Annotations/{}.xml'.format(root_dir_voc, filename.split('.')[0])

  170.             writexml(filename, img, bboxes, xmlpath)

  171.             print('success number is %d' % index)

  172.             index += 1
  173.         # 循环结束后
  174.         print('所有没有人脸的索引:', no_face_index)

  175.     fwrite.close()


  176. if __name__ == '__main__':
  177.     # num of train images :12879 所有没有人脸的索引: [279, 3808, 7512, 9227]
  178.     # convert_imgset('train')
  179.     # num of val images : 3225 所有没有人脸的索引: []
  180.     convert_imgset('val')
  181.     # 修改文件名  原本是 train.txt  val.txt
  182.     # shutil.move(root_dir_voc + '/ImageSets/Main/' + 'train.txt', root_dir_voc + '/ImageSets/Main/' + 'trainval.txt')
  183.     # shutil.move(root_dir_voc + '/ImageSets/Main/' + 'val.txt', root_dir_voc + '/ImageSets/Main/' + 'test.txt')





复制代码

之后利用caffe-ssd源码下data目录下的脚本(create_list.sh   create_data.sh) 将voc格式转换为LMDB格式数据集:
针对人脸数据集对训练脚本的修改:
在caffe-ssd源码下examples/ssd/ssd_pascal.py基础上修改 复制创建一个新文件ssd_face_dfy.py
我修改了一些配置参数,更多优化的地方大家可以尝试:
1、数据集的修改:
train_data = "/home/dfy888/DataSets/DataSets_LMDB/WiderFace/lmdb/WiderFace_trainval_lmdb"
test_data = "/home/dfy888/DataSets/DataSets_LMDB/WiderFace/lmdb/WiderFace_test_lmdb"
2、类别数修改(人脸的label_map_file文件 0为背景 1为人脸)
num_classes = 2
3、GPUs的修改

4、solver_param网络超参配置的修改
  1. solver_param = {
  2.     # Train parameters
  3.     'base_lr': base_lr,
  4.     'weight_decay': 0.0005,
  5.     'lr_policy': "multistep",
  6.     'stepvalue': [4000, 10000, 40000],
  7.     'gamma': 0.1,
  8.     'momentum': 0.9,
  9.     'iter_size': iter_size,
  10.     'max_iter': 80000,
  11.     'snapshot': 500,
  12.     'display': 10,
  13.     'average_loss': 10,
  14.     # types: AdaDelta, AdaGrad, Adam, Nesterov, RMSProp, SGD)
  15.     'type': "Adam",
  16.     'solver_mode': solver_mode,
  17.     'device_id': device_id,
  18.     'debug_info': False,
  19.     'snapshot_after_train': True,
  20.     # Test parameters
  21.     'test_iter': [test_iter],
  22.     'test_interval': 500,
  23.     'eval_type': "detection",
  24.     'ap_version': "11point",
  25.     'test_initialization': False,
  26. }
复制代码

5、det_out_param检测层输出参数修改

6、主干网络的修改
路径在caffe-ssd源码下python/caffe/model_libs.py里的某个函数实现
VGGNetBody_Dfy(net, from_layer='data', nopool=False, dilate_pool4=False)
  1. def VGGNetBody_Dfy(net, from_layer, nopool=False, freeze_layers=[], dilate_pool4=False):

  2.     print '来自哪个层from_layer:' + from_layer
  3.     kwargs = {
  4.         'param': [dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)],
  5.         'weight_filler': dict(type='xavier'),
  6.         'bias_filler': dict(type='constant', value=0)}

  7.     assert from_layer in net.keys()
  8.     net.conv1_1 = L.Convolution(net[from_layer], num_output=32, pad=1, kernel_size=3, **kwargs)

  9.     print type(net.conv1_1)

  10.     net.relu1_1 = L.ReLU(net.conv1_1, in_place=True)
  11.     net.conv1_2 = L.Convolution(net.relu1_1, num_output=32, pad=1, kernel_size=3, **kwargs)
  12.     net.relu1_2 = L.ReLU(net.conv1_2, in_place=True)

  13.     if nopool:
  14.         name = 'conv1_3'
  15.         net[name] = L.Convolution(net.relu1_2, num_output=32, pad=1, kernel_size=3, stride=2, **kwargs)
  16.     else:
  17.         name = 'pool1'
  18.         net.pool1 = L.Pooling(net.relu1_2, pool=P.Pooling.MAX, kernel_size=2, stride=2)
  19.     net.conv2_1 = L.Convolution(net[name], num_output=64, pad=1, kernel_size=3, **kwargs)
  20.     net.relu2_1 = L.ReLU(net.conv2_1, in_place=True)
  21.     net.conv2_2 = L.Convolution(net.relu2_1, num_output=64, pad=1, kernel_size=3, **kwargs)
  22.     net.relu2_2 = L.ReLU(net.conv2_2, in_place=True)

  23.     if nopool:
  24.         name = 'conv2_3'
  25.         net[name] = L.Convolution(net.relu2_2, num_output=64, pad=1, kernel_size=3, stride=2, **kwargs)
  26.     else:
  27.         name = 'pool2'
  28.         net[name] = L.Pooling(net.relu2_2, pool=P.Pooling.MAX, kernel_size=2, stride=2)
  29.     net.conv3_1 = L.Convolution(net[name], num_output=128, pad=1, kernel_size=3, **kwargs)
  30.     net.relu3_1 = L.ReLU(net.conv3_1, in_place=True)
  31.     # net.conv3_2 = L.Convolution(net.relu3_1, num_output=128, pad=1, kernel_size=3, **kwargs)
  32.     # net.relu3_2 = L.ReLU(net.conv3_2, in_place=True)
  33.     net.conv3_3 = L.Convolution(net.relu3_1, num_output=128, pad=1, kernel_size=3, **kwargs)
  34.     net.relu3_3 = L.ReLU(net.conv3_3, in_place=True)

  35.     if nopool:
  36.         name = 'conv3_4'
  37.         net[name] = L.Convolution(net.relu3_3, num_output=128, pad=1, kernel_size=3, stride=2, **kwargs)
  38.     else:
  39.         name = 'pool3'
  40.         net[name] = L.Pooling(net.relu3_3, pool=P.Pooling.MAX, kernel_size=2, stride=2)
  41.     net.conv4_1 = L.Convolution(net[name], num_output=256, pad=1, kernel_size=3, **kwargs)
  42.     net.relu4_1 = L.ReLU(net.conv4_1, in_place=True)
  43.     # net.conv4_2 = L.Convolution(net.relu4_1, num_output=256, pad=1, kernel_size=3, **kwargs)
  44.     # net.relu4_2 = L.ReLU(net.conv4_2, in_place=True)
  45.     net.conv4_3 = L.Convolution(net.relu4_1, num_output=256, pad=1, kernel_size=3, **kwargs)
  46.     net.relu4_3 = L.ReLU(net.conv4_3, in_place=True)

  47.     if nopool:
  48.         name = 'conv4_4'
  49.         net[name] = L.Convolution(net.relu4_3, num_output=256, pad=1, kernel_size=3, stride=2, **kwargs)
  50.     else:
  51.         name = 'pool4'
  52.         if dilate_pool4:
  53.             # 这种最大池化 会有信息的重叠 会有卷积的膨胀操作
  54.             net[name] = L.Pooling(net.relu4_3, pool=P.Pooling.MAX, kernel_size=3, stride=1, pad=1)
  55.             dilation = 2
  56.         else:
  57.             # 这种最大池化 不会重叠 直接降采样一半
  58.             net[name] = L.Pooling(net.relu4_3, pool=P.Pooling.MAX, kernel_size=2, stride=2)
  59.             dilation = 1
  60.     kernel_size = 3
  61.     # 计算需要pad的值
  62.     pad = int((kernel_size + (dilation - 1) * (kernel_size - 1)) - 1) / 2
  63.     # 卷积的参数dilation是什么意思?lr_mult  decay_mult
  64.     net.conv5_1 = L.Convolution(net[name], num_output=256, pad=pad, kernel_size=kernel_size, dilation=dilation,
  65.                                 **kwargs)
  66.     net.relu5_1 = L.ReLU(net.conv5_1, in_place=True)
  67.     # net.conv5_2 = L.Convolution(net.relu5_1, num_output=256, pad=pad, kernel_size=kernel_size, dilation=dilation,
  68.     #                             **kwargs)
  69.     # net.relu5_2 = L.ReLU(net.conv5_2, in_place=True)
  70.     net.conv5_3 = L.Convolution(net.relu5_1, num_output=256, pad=pad, kernel_size=kernel_size, dilation=dilation,
  71.                                 **kwargs)
  72.     net.relu5_3 = L.ReLU(net.conv5_3, in_place=True)

  73.     # if need_fc:

  74.     # Update freeze layers.
  75.     # 卷积的这两个参数lr_mult  decay_mult决定:是否冻结该层
  76.     kwargs['param'] = [dict(lr_mult=0, decay_mult=0), dict(lr_mult=0, decay_mult=0)]

  77.     layers = net.keys()
  78.     for freeze_layer in freeze_layers:
  79.         if freeze_layer in layers:
  80.             net.update(freeze_layer, kwargs)
  81.     return net
复制代码

7、prior box层输入的修改:
6种尺寸的feature_map(38*38 19*19 10*10 5*5 3*3 1*1)
mbox_source_layers = ['conv4_3', 'conv5_3', 'conv6_2', 'conv7_2', 'conv8_2', 'conv9_2']
steps = [8, 16, 32, 64, 100, 300] 则是相对于原图300*300的下采样的倍数
aspect_ratios = [[2], [2, 3], [2, 3], [2, 3], [2], [2]] 长宽比
min_sizes   max_sizes
normalizations = [20, -1, -1, -1, -1, -1] 只对conv4_3进行正则化 因为尺寸最大 其他都不进行(-1)
num_priors_per_location(每个anchor为中心提取的priors box个数):[4, 6, 6, 6, 4, 4]
则总共会生成(38*38*4+19*19*6+10*10*6+5*5*6+3*3*4+1*1*4=8732)个pirors box

执行:python examples/ssd/ssd_face_dfy.py 开始模型的训练

使用已经训练好的模型进行预测:
创建文件ssd_face_dfy_predict.py 暂不分享 有问题联系东方耀微信:dfy_88888
执行python examples/ssd/ssd_face_dfy_results/ssd_face_dfy_predict.py 查看能否检测到所有人脸及其置信度

结论:随着训练次数的增加 模型检测人脸越来越精准 置信度也越来越高
使用同一张我在地铁中拍摄的图片进行预测(置信度阈值为0.3
1、当使用训练了2677次caffemodel时能检测出2个人脸 他们的置信度分别为:[0.45893413, 0.3917135 ]
2、当使用训练了5405次caffemodel时能检测出2个人脸 他们的置信度分别为:[0.51490724, 0.40424186]
3、当使用训练了6375次caffemodel时能检测出3个人脸 他们的置信度分别为:[0.5186607, 0.4068523, 0.3596757]


4、当使用训练了13705次caffemodel时能检测出3个人脸 他们的置信度分别为:[0.8050997, 0.4799765, 0.4243071]

5、当使用训练了24357次caffemodel时能检测出3个人脸 他们的置信度分别为:[0.84193945, 0.51308644, 0.3932887 ]
6、当使用训练了35557次caffemodel时能检测出3个人脸 他们的置信度分别为:[0.90560204, 0.63326204, 0.38992488]
继续训练并预测中。。。


SSD人脸检测模型继续优化的思路
1、数据打包的时候,可以考虑过滤掉小人脸样本(要求人脸size大于20像素)
2、训练样本数据规模尽可能大,可以尝试获取合并多个不同数据集
3、采用resnet+FPN等特征主干网络,如果是在终端设备中用则考虑轻量级mobilenet等
4、采用更好的LoSS进行训练(原ssd:分类用softmax 回归用smooth L1)
5、等等其他,以后想出来再补充!














widerface.png (601.21 KB, 下载次数: 113)

widerface.png

voc2lmdb.png (38.76 KB, 下载次数: 114)

voc2lmdb.png

6375.caffemodel.png (1.05 MB, 下载次数: 114)

6375.caffemodel.png

ssd-widerface-train.png (240.9 KB, 下载次数: 117)

ssd-widerface-train.png

13705.caffemodel.png (1.05 MB, 下载次数: 117)

13705.caffemodel.png

抗攻击人脸数据采集的方法.jpg (139.7 KB, 下载次数: 110)

抗攻击人脸数据采集的方法.jpg

基于人脸的业务场景.jpg (51.75 KB, 下载次数: 111)

基于人脸的业务场景.jpg
让天下人人学会人工智能!人工智能的前景一片大好!
回复

使用道具 举报

0

主题

117

帖子

258

积分

中级会员

Rank: 3Rank: 3

积分
258
QQ
沙发
发表于 2020-2-3 15:48:11 | 只看该作者
谢谢老师提供的资料。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|人工智能工程师的摇篮 ( 湘ICP备2020019608号-1 )

GMT+8, 2024-4-26 18:26 , Processed in 0.196722 second(s), 21 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表