机器学习实战篇-人脸识别(1)- 人脸定位

来源:互联网 发布:多功能水枪 数据 编辑:程序博客网 时间:2024/06/06 17:30

目的与过程概要

  • 1.目的:输入一张图片,让机器在人脸的位置画出一个框
    这里写图片描述

  • 2.过程概要

    • 训练一个能识别一张227*227的图像是否是人脸的二分类模型(使用AlexNet网络)
      这里写图片描述=>人脸
      这里写图片描述=>非人脸
    • 修改训练好的网络模型,数据层改为输入层,全链接层改为全卷积层(起到窗口滑动的作用)
    • 将输入的图片进行放大缩小变换scal变换

    - 根据图像的大小,动态的修改网络模型的数据层

环境

首先,要安装以下环境

  • Ubuntu:
  • python
  • anaconda:机器学习的python环境,包含了许多必要的库,比如numpy
  • opencv:机器视觉常用库
  • caffe :网络训练的基础
  • cuda:如果用Gpu 运行,需要安装的包

第一步:数据准备

1.标记好的数据

一般这些数据网上都有(http://blog.csdn.net/chenriwei2/article/details/50631212),不用我们自己制作。如果得到的是原始的数据(一张完整的图,指定人脸的区域),那么就需要进行样本采样
- 裁剪工具:http://www.jianshu.com/p/856d1d420854,或者使用opencv裁剪

2.正负样本采样

- 正样本采样:即人脸的部分 ,需要把图片中人脸的部分裁剪出来,要注意的是,裁剪出来后的图片要人工过一遍,数据的好坏对训练的结果影响很大。- 负样本采样:在非人脸的部分进行随机的采样    负样本的采样比较复杂,先随机在图片上取图,然后计算与人脸部分的iOU,即重叠率,设定一个阈值,小于这个阈值的就认为是非人脸     - IOU: http://blog.csdn.net/eddy_zheng/article/details/52126641

3.制作lmdb数据

lmdb是caffe的训练数据格式,制作lmdb数据需要准备图片数据和标签数据
- 图片数据是我们上面裁剪好和做好分类的图片
- 标签数据是txt文件,格式是 图片路径+空格+标签 如:1/23039_nonface_0image30595.jpg 1
- 把数据集切分成训练集(train)和测试集合(val)
- 使用以下代码进行lmdb数据的生成

#!/usr/bin/env shEXAMPLE=~/code/learn # 输出的文件夹根目录DATA=~/code/learn #存放标签数据的根目录,该文件夹下有对应的标签数据TOOLS=~/code/caffe/build/tools  # caffe安装目录的tools文件夹TRAIN_DATA_ROOT=~/code/learn/train/train/ # 存放训练数据集的目录VAL_DATA_ROOT=~/code/learn/train/val/ # 存放测试数据集的目录#resize图片的大小为227*227RESIZE=trueif $RESIZE; then  RESIZE_HEIGHT=227  RESIZE_WIDTH=227else  RESIZE_HEIGHT=0  RESIZE_WIDTH=0fiif [ ! -d "$TRAIN_DATA_ROOT" ]; then  echo "Error: TRAIN_DATA_ROOT is not a path to a directory: $TRAIN_DATA_ROOT"  echo "Set the TRAIN_DATA_ROOT variable in create_face_48.sh to the path" \       "where the face_48 training data is stored."  exit 1fiif [ ! -d "$VAL_DATA_ROOT" ]; then  echo "Error: VAL_DATA_ROOT is not a path to a directory: $VAL_DATA_ROOT"  echo "Set the VAL_DATA_ROOT variable in create_face_48.sh to the path" \       "where the face_48 validation data is stored."  exit 1fiecho "Creating train lmdb..."# 生成训练集lmdb,生成结果在 $EXAMPLE/face_train_lmdbGLOG_logtostderr=1 $TOOLS/convert_imageset \    --resize_height=$RESIZE_HEIGHT \    --resize_width=$RESIZE_WIDTH \    --shuffle \    $TRAIN_DATA_ROOT \    $DATA/train.txt \    $EXAMPLE/face_train_lmdbecho "Creating val lmdb..."# 生成测试集lmdb,生成结果在 $EXAMPLE/face_val_lmdbGLOG_logtostderr=1 $TOOLS/convert_imageset \    --resize_height=$RESIZE_HEIGHT \    --resize_width=$RESIZE_WIDTH \    --shuffle \    $VAL_DATA_ROOT \    $DATA/val.txt \    $EXAMPLE/face_val_lmdbecho "Done."Status API Training Shop Blog About

4.结果

经过第一步,你应该获取的最终结果是
1.训练集合的lmdb文件:face_train_lmdb文件夹对应的data.mdb和lock.mdb
2.测试集合的lmdb文件:face_val_lmdb文件夹对应的data.mdb和lock.mdb

第二步:训练一个识别图片是否人脸的神经网络

在准备好了数据之后,第二步是训练一个能够识别一张227*227的图片是否是人脸的神经网络

1.网络模型配置

在这里我们不准备讲解这些具体的神经网络,假如你不知道什么是卷积,relu,池化,全连接层的话,你直接使用这些网络配置文件就好了,我们在这里使用的是AlexNet网络(AlexNet:参考http://blog.csdn.net/chaipp0607/article/details/72847422)

  • caffe 网络配置 :train.prototxt
    train.prototxt文件是定义网络模型的文件, 需要修改的是lmdb数据的路径,对应的训练集和测试集的lmdb数据,以及减均值的路径
############################  注意:这里只是train.prototxt文件的一部分,你需要下载完整的train.prototxt  #############################layer {  top: "data"  top: "label"  name: "data"  type: "Data"  data_param {    source: "/home/tas/code/learn/face_train_lmdb" #训练集的lmdb路径    backend:LMDB    batch_size: 64  }  transform_param {     #mean_file: "/home/tas/code/caffe/data/ilsvrc12/imagenet_mean.binaryproto" # caffe安装目录对应的文件,用于减均值计算     mirror: true   }  include: { phase: TRAIN }}
  • 运行配置:solver.prototxt
    参考:https://www.cnblogs.com/denny402/p/5074049.html
net: "/home/tas/code/learn/train.prototxt" # 定义的网络模型test_iter: 100 # 测试时迭代的次数,batch_size(在train.prototxt定义)*test_iter要等于测试集合的大小test_interval: 500 # 每训练500次进行一次测试# lr for fine-tuning should be lower than when starting from scratchbase_lr: 0.001 # 基础学习率lr_policy: "step"gamma: 0.1# stepsize should also be lower, as we're closer to being donestepsize: 20000display: 100max_iter: 100000 # 训练的次数momentum: 0.9weight_decay: 0.0005snapshot: 10000 # 每训练10000次保存一次模型snapshot_prefix: "/home/tas/code/learn/model/" # 最后生成模型的保存路径# uncomment the following to default to CPU mode solvingsolver_mode: GPU # 这里使用GPU的话需要安装CUDA等环境,并且caffe编译时要注释掉CPU_only,否则使用CPU
  • 运行文件:train.sh
#!/usr/bin/env sh/home/tas/code/caffe/build/tools/caffe train --solver=/home/tas/code/learn/solver.prototxt \#--snapshot=/home/tas/code/learn/model/_iter_72484.solverstate \ # 如果要接着上次的训练结果据需运行,取消注释这行,并制定到对应上次训练后生成的文件#--gpu all # GPU模式取消注释这行
  • 执行训练,打开终端,进入到train.sh的目录,在命令行里敲入以下代码就开始训练了
sh train.sh

2.防止过拟合

在我们训练的过程中,可能出现过拟合的情况,过拟合的情况就是在训练集里的效果很好,准确率很高,但是在测试集的测试的结果却很差,我们可以挑选效果最好的model,调低基础学习率,再次训练

3.GPU运行

- 安装CUDA - caffe 中Makefile.config 注释 CPU_only,重新编译- 设置GPU模式:solver.prototxt-  train.sh选用GPU

4.结果

经过第二步,你得到的结果应该是一个.caffemodel文件

第三步,编写代码

1.修改模型

  • 在写代码前,我们需要先调整下网络模型train.prototxt,修改后的文件为deploy_full_conv.prototxt,调整的目的
    • 删除数据层,修改为输入层
      由于我们现在没有数据的,每次输入一张图片输入模型进行运算,需要先删除掉data层,改为如下的代码
name: "CaffeNet_full_conv"input: "data" input_dim: 1  # 每次输入一张图片input_dim: 3    # 图片的RPG三通道input_dim: 500  # 图片的宽input_dim: 500  # 图片的高
  • 把全链接层改为全卷积层达到窗口滑动的效果。
    训练好的模型只能识别227*227大小的图片,我们需要把全连接层改为全卷积层,这样子能够达到一个窗口滑动的效果,扫描整张图片。所以就会的输出结果应该是多个结果的概率矩阵。
    修改全连接层只需要把对应的layer层的type从InnerProduct 修改为 Convolution,并且修改全连接的参数inner_product_param为卷积的参数convolution_param,具体的参数是一样的,只需要再增加一个卷积核大小的参数kernel_size
    修改前的第六层
layer {  name: "fc6"  type: "InnerProduct"  bottom: "pool5"  top: "fc6"  param {    lr_mult: 1    decay_mult: 1  }  param {    lr_mult: 2    decay_mult: 0  }  inner_product_param {    num_output: 4096    weight_filler {      type: "gaussian"      std: 0.005    }    bias_filler {      type: "constant"      value: 0.1    }  }}

修改后

layer {  name: "fc6-conv"  type: "Convolution"  bottom: "pool5"  top: "fc6-conv"  param {    lr_mult: 1    decay_mult: 1  }  param {    lr_mult: 2    decay_mult: 0  }  convolution_param {    num_output: 4096    kernel_size: 6    weight_filler {      type: "gaussian"      std: 0.005    }    bias_filler {      type: "constant"      value: 1    }  }}

同样对其他两层全连接层做一样的操作
- 删除两层pool层,增加计算精度
- 删除accuracy层和loss层,因为我们已经不需要计算精度了,我们只需要一个结果
- 增加Softmax层,将计算结果转化为概率输出

2.图片的scal变换

上面训练的模型只能识别一个227*227大小的,但是输入的图片内人脸的大小不一定是这么大,有可能偏大500*500,或者偏小50*50,所以需要对原图多次进行缩放后才作为结果输入,这样子总有一张图的头像区域的大小是接近227*227的。

3. 动态修改模型

由于每张输入的图片大小都可能不一样,需要动态的修改输入层图片的大小

4.非最大值抑制(NMS)

一个人脸可能被多次识别,但是我们只需要一个最准确的结果就可以了,
这里写图片描述
取概率最大值后
这里写图片描述
具体可参考:http://blog.csdn.net/shuzfan/article/details/52711706
或者直接使用以下的代码:并最终调用nms_average(boxes_nums, 1, 0.2)
boxes_nums 是模型数据的结果,

class Point(object):    def __init__(self, x, y):        self.x = x        self.y = ydef calculateDistance(x1,y1,x2,y2):    dist = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)    return distdef range_overlap(a_min, a_max, b_min, b_max):    return (a_min <= b_max) and (b_min <= a_max)def rect_overlaps(r1,r2):    return range_overlap(r1.left, r1.right, r2.left, r2.right) and range_overlap(r1.bottom, r1.top, r2.bottom, r2.top)def rect_merge(r1,r2, mergeThresh):    if rect_overlaps(r1,r2):        # dist = calculateDistance((r1.left + r1.right)/2, (r1.top + r1.bottom)/2, (r2.left + r2.right)/2, (r2.top + r2.bottom)/2)        SI= abs(min(r1.right, r2.right) - max(r1.left, r2.left)) * abs(max(r1.bottom, r2.bottom) - min(r1.top, r2.top))        SA = abs(r1.right - r1.left)*abs(r1.bottom - r1.top)        SB = abs(r2.right - r2.left)*abs(r2.bottom - r2.top)        S=SA+SB-SI        ratio = float(SI) / float(S)        if ratio > mergeThresh :            return 1    return 0class Rect(object):    def __init__(self, p1, p2):        '''Store the top, bottom, left and right values for points               p1 and p2 are the (corners) in either order        '''        self.left   = min(p1.x, p2.x)        self.right  = max(p1.x, p2.x)        self.bottom = min(p1.y, p2.y)        self.top    = max(p1.y, p2.y)    def __str__(self):        return "Rect[%d, %d, %d, %d]" % ( self.left, self.top, self.right, self.bottom )def nms_average(boxes, groupThresh=2, overlapThresh=0.2):    rects = []    temp_boxes = []    weightslist = []    new_rects = []    for i in range(len(boxes)):        if boxes[i][4] > 0.2:            rects.append([boxes[i,0], boxes[i,1], boxes[i,2]-boxes[i,0], boxes[i,3]-boxes[i,1]])    rects, weights = cv2.groupRectangles(rects, groupThresh, overlapThresh)    rectangles = []    for i in range(len(rects)):        testRect = Rect( Point(rects[i,0], rects[i,1]), Point(rects[i,0]+rects[i,2], rects[i,1]+rects[i,3]))        rectangles.append(testRect)    clusters = []    for rect in rectangles:        matched = 0        for cluster in clusters:            if (rect_merge( rect, cluster , 0.2) ):                matched=1                cluster.left   =  (cluster.left + rect.left   )/2                cluster.right  = ( cluster.right+  rect.right  )/2                cluster.top    = ( cluster.top+    rect.top    )/2                cluster.bottom = ( cluster.bottom+ rect.bottom )/2        if ( not matched ):            clusters.append( rect )    result_boxes = []    for i in range(len(clusters)):        result_boxes.append([clusters[i].left, clusters[i].bottom, clusters[i].right, clusters[i].top, 1])    return result_boxes

人脸坐标映射

由于最终结果是一个概率点,我们需要根据网络模型结构把它映射回原图

def GenrateBoundingBox(featureMap, scale):    boundingBox = []    stride = 32 # 可以把网络结构进行了32倍卷积    cellSize = 227 #滑动窗口的大小    for (x, y), prob in np.ndenumerate(featureMap):        if prob>0.95:            boundingBox.append([float(stride*y)/scale, float(stride*x)/scale,                               float(stride * y+ cellSize - 1) / scale, float(stride*x+ cellSize - 1)/scale,                               prob])    return boundingBox

完整的代码

  • 注意:这里的”/home/tas/code/”是我本机的路径,根据你自己的路径进行修改
# -*- coding: utf-8 -*-import sysimport osfrom math import powfrom PIL import Image, ImageDraw,ImageFontimport cv2import mathimport randomimport numpy as npcaffe_root = '/home/tas/code/caffe/'sys.path.insert(0, caffe_root+'python')# 设置log等级os.environ['GLOG_minloglevel'] = '2'import caffecaffe.set_mode_gpu()temp_path =  '/home/tas/code/learn/temp_img/'def face_detection(imgFile):# 这里调用的是第二步生成的模型和第三步修改后的神经网络    net_full_conv = caffe.Net('/home/tas/code/learn/deploy_full_conv.prototxt',                              '/home/tas/code/learn/alexnet_iter_50000_full_conv.caffemodel',                              caffe.TEST)    scales = [] # 刻度    factor = 0.79 # 变换的倍数    img = cv2.imread(imgFile)    # 最大倍数    largest = min(2, 4000/max(img.shape[0:2]))    # 最小的边的长度    minD = largest*min(img.shape[0:2])    scale = largest    # 从最大到最小227,获取变换的倍数    while minD >= 227:        scales.append(scale)        scale *= factor        minD *= factor    # 存储人脸图    total_box = []    # 变换图片    for scale in scales:        fileName = "img_"+str(scale)+'.jpg'        scale_img = cv2.resize(img, (int((img.shape[0]*scale)), int(img.shape[1]*scale)))        cv2.imwrite(temp_path+fileName, scale_img)        im = caffe.io.load_image(temp_path+fileName)        # 动态修改数据层的大小?这里为什么时1,0 而不是0,1        net_full_conv.blobs['data'].reshape(1, 3, scale_img.shape[1], scale_img.shape[0])        transformer = caffe.io.Transformer({'data':net_full_conv.blobs['data'].data.shape})        # 减均值,归一化        transformer.set_mean('data', np.load(caffe_root+'python/caffe/imagenet/ilsvrc_2012_mean.npy'))        # 维度变换 ,cafee默认的时BGR格式,要把RGB(0,1,2)改为BGR(2,0,1)        transformer.set_transpose('data', (2, 0, 1))        # 像素        transformer.set_raw_scale('data', 255)        transformer.set_channel_swap('data', (2, 1, 0))        # 人脸坐标映射        # 前先传播,映射到原始图像的位置        out = net_full_conv.forward_all(data=np.asarray(transformer.preprocess('data', im)))        #out['prob'][0, 1] 0表示类别,1表示概率        boxes = GenrateBoundingBox(out['prob'][0, 1], scale)        if(boxes):            total_box.extend(boxes)    boxes_nums = np.array(total_box)    #nms 处理    true_boxes = nms_average(boxes_nums, 1, 0.2)    if not true_boxes == []:        x1,y1,x2,y2 = true_boxes[0][:-1]        cv2.rectangle(img,(int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), thickness=5)        cv2.imwrite('/home/tas/code/learn/result_img/result.jpg', img)        # cv2.imshow('test', img)def GenrateBoundingBox(featureMap, scale):    boundingBox = []    stride = 32    cellSize = 227 #滑动窗口的大小    for (x, y), prob in np.ndenumerate(featureMap):        if prob>0.95:            boundingBox.append([float(stride*y)/scale, float(stride*x)/scale,                               float(stride * y+ cellSize - 1) / scale, float(stride*x+ cellSize - 1)/scale,                               prob])    return boundingBoxclass Point(object):    def __init__(self, x, y):        self.x = x        self.y = ydef calculateDistance(x1,y1,x2,y2):    dist = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)    return distdef range_overlap(a_min, a_max, b_min, b_max):    return (a_min <= b_max) and (b_min <= a_max)def rect_overlaps(r1,r2):    return range_overlap(r1.left, r1.right, r2.left, r2.right) and range_overlap(r1.bottom, r1.top, r2.bottom, r2.top)def rect_merge(r1,r2, mergeThresh):    if rect_overlaps(r1,r2):        # dist = calculateDistance((r1.left + r1.right)/2, (r1.top + r1.bottom)/2, (r2.left + r2.right)/2, (r2.top + r2.bottom)/2)        SI= abs(min(r1.right, r2.right) - max(r1.left, r2.left)) * abs(max(r1.bottom, r2.bottom) - min(r1.top, r2.top))        SA = abs(r1.right - r1.left)*abs(r1.bottom - r1.top)        SB = abs(r2.right - r2.left)*abs(r2.bottom - r2.top)        S=SA+SB-SI        ratio = float(SI) / float(S)        if ratio > mergeThresh :            return 1    return 0class Rect(object):    def __init__(self, p1, p2):        '''Store the top, bottom, left and right values for points               p1 and p2 are the (corners) in either order        '''        self.left   = min(p1.x, p2.x)        self.right  = max(p1.x, p2.x)        self.bottom = min(p1.y, p2.y)        self.top    = max(p1.y, p2.y)    def __str__(self):        return "Rect[%d, %d, %d, %d]" % ( self.left, self.top, self.right, self.bottom )def nms_average(boxes, groupThresh=2, overlapThresh=0.2):    rects = []    temp_boxes = []    weightslist = []    new_rects = []    for i in range(len(boxes)):        if boxes[i][4] > 0.2:            rects.append([boxes[i,0], boxes[i,1], boxes[i,2]-boxes[i,0], boxes[i,3]-boxes[i,1]])    rects, weights = cv2.groupRectangles(rects, groupThresh, overlapThresh)    rectangles = []    for i in range(len(rects)):        testRect = Rect( Point(rects[i,0], rects[i,1]), Point(rects[i,0]+rects[i,2], rects[i,1]+rects[i,3]))        rectangles.append(testRect)    clusters = []    for rect in rectangles:        matched = 0        for cluster in clusters:            if (rect_merge( rect, cluster , 0.2) ):                matched=1                cluster.left   =  (cluster.left + rect.left   )/2                cluster.right  = ( cluster.right+  rect.right  )/2                cluster.top    = ( cluster.top+    rect.top    )/2                cluster.bottom = ( cluster.bottom+ rect.bottom )/2        if ( not matched ):            clusters.append( rect )    result_boxes = []    for i in range(len(clusters)):        result_boxes.append([clusters[i].left, clusters[i].bottom, clusters[i].right, clusters[i].top, 1])    return result_boxesface_detection('/home/tas/code/learn/result_img/timg.jpeg')

测试

最后,调用函数,就会生成一张倍圈中人脸的图片

face_detection('test.jpg')

问题

1.在哪里进行窗口滑动:将全链接层改为全卷积层
2.为什么要用全卷积层替换全链接层
参考:http://blog.csdn.net/nnnnnnnnnnnny/article/details/70194432

文件和数据下载

链接: https://pan.baidu.com/s/1kUE2B7D 密码: dmxe
数据缺失请留言

原创粉丝点击