机器学习实战篇-人脸识别(1)- 人脸定位
来源:互联网 发布:多功能水枪 数据 编辑:程序博客网 时间:2024/06/06 17:30
目的与过程概要
1.目的:输入一张图片,让机器在人脸的位置画出一个框
2.过程概要
- 训练一个能识别一张227*227的图像是否是人脸的二分类模型(使用AlexNet网络)
=>人脸
=>非人脸 - 修改训练好的网络模型,数据层改为输入层,全链接层改为全卷积层(起到窗口滑动的作用)
- 将输入的图片进行放大缩小变换scal变换
- 根据图像的大小,动态的修改网络模型的数据层
- 训练一个能识别一张227*227的图像是否是人脸的二分类模型(使用AlexNet网络)
环境
首先,要安装以下环境
- 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
数据缺失请留言
- 机器学习实战篇-人脸识别(1)- 人脸定位
- 机器学习&人脸识别
- 机器学习-人脸识别
- 人脸识别中的机器学习
- 【机器学习实践】人脸识别模型结果对比
- 【机器学习实践】人脸识别模型结果对比
- 【从零学习openCV】IOS7人脸识别实战
- 深度学习实战——人脸识别
- 数学之路(3)-机器学习(3)-机器学习算法-人脸识别
- 机器学习xgboost实战—手写数字识别 (DMatrix)
- 15分钟实战机器学习:验证码(CAPTCHA)识别
- 人脸识别眼睛定位算法
- 机器学习实战之程序清单1-kNN(手写数字识别系统)
- 机器学习实战kNN之手写识别
- 机器学习实战kNN之手写识别
- 机器学习实战kNN之手写识别
- 机器学习实战-knn_手写识别
- 【机器学习实战02】手写识别系统
- org.apache.catalina.core.StandardWrapperValve invoke的解决办法
- usaco6.4.2 Electric Fences
- 德语词汇笔记(一)
- 微服务注册与发现及如何使用Eureka
- [LeetCode]712. Minimum ASCII Delete Sum for Two Strings
- 机器学习实战篇-人脸识别(1)- 人脸定位
- ICMP协议
- nagios插件之检测tomcat日志的startAt值(待改进)
- 趣图:程序员调 Bug 的 5 个阶段
- 数据库的完整性
- 老说程序员如何看产品经理,今天说说产品经理讨厌哪些程序员
- ARPA网
- 高级程序设计——高级定时器
- LeetCode 120. Triangle