1. 背景
对于很多初⼊深度学习计算机视觉领域的朋友来说,当前开源资料⾮常多,但有时候难以适从,其中很多资料都没有包含完整的项⽬流程,⽽只是对某个流程的部分截取,对能⼒的锻炼不够。图像分类是整个计算机视觉领域中最基础的任务,也是最重要的任务之⼀,最适合拿来进⾏学习实践。为了让新⼿们能够⼀次性体验⼀个⼯业级别的图像分类任务的完整流程,本次我们选择带领⼤家完成⼀个对图片中⼈脸进⾏表情识别的任务。
⼈脸表情识别(facial expression recognition, FER)作为⼈脸识别技术中的⼀个重要组成部分,近年来在⼈机交互、安全、机器⼈制造、⾃动化、医疗、通信和驾驶领域得到了⼴泛的关注,成为学术界和⼯业界的研究热点,是⼈脸属性分析的重点。
2. 数据获取
很多实际项⽬我们不会有现成的数据集,虽然可以通过开源数据集获取,但是我们还是要学会⾃⼰从零开始获取和整理。下⾯讲述如何准备好本次项⽬所需要的数据集,包括以下部分:
- 学会使⽤爬⾍爬取图像。
- 对获得的图⽚数据进⾏整理,包括重命名,格式统⼀。
- 利⽤⼈脸检测算法删选出有⽤的样本,利⽤关键点检测算法裁剪出⽤于算法训练的嘴唇区域。
2.1 数据爬取
由于没有直接对应的开源数据集,或者开源数据集中的数据⽐较少,尤其是对于嘟嘴,⼤笑等类的数据。搜索引擎上有海量数据,所以我们可以从中爬取。下⾯开始讲述具体的步骤,我们的任务是⼀个表情分类任务,因此需要爬取相关图⽚,包括嘟嘴(pout),微笑(smile),⼤笑(openmouth)、无表情(none)等表情。
本项目使用的爬虫项目是:https://github.com/sczhengyabin/Image-Downloader ,可以按要求爬取百度、Bing、Google 上的图片,提供了非常人性化的 GUI 方便操作,使用方法如下:
- 下载爬虫工具
- 将代码导入到 PyCharm
- 运行 python image_downloader_gui.py 调用GUI界面
- 配置好参数(关键词,路径,爬取数目等),关键词可以直接在这里输入也可以选择从txt文件中选择。该项目的 GUI 界面如下,我们尝试爬取“正面嘟嘴高清照”的相关表情
- 爬取图⽚结果如下
⾃此就可以获得1000张图像,然后进⾏第⼆步,数据整理。
TIPS:读者可以下载 https://github.com/sczhengyabin/Image-Downloader 这个项目中的爬虫工具,在自己的电脑尝试一下数据爬取,相信通过操作,会加深你的印象
3. 数据整理
爬取得到的数据是⽐较脏的,需要进⾏整理,主要包括统⼀图⽚后缀和重命名。统⼀后缀格式可以减少以后写数据 API 时的压⼒,也可以测试图⽚是不是可以正常的读取,及时防⽌未知问题的出现,这很重要。
新建 Emotion_Recognition_File(表情识别的英文名称)文件夹,对 Emotion_Recognition_File 文件夹以下对各个文件进行说明
- img_type_test:放置了不同后缀名的图片,在 3.1 图片格式统一 将会使用到
- face_detect_model:放置了人脸检测所需要的模型,在 3.2 数据清洗、3.3 提取嘴唇区域 将会使用到
- face_det_img:放置了一些包含人脸的图片和不包含人人脸的图片,在 3.2 数据清洗 将会使用到
- mouth_det_img:放置了一些包含人脸的图片,在 3.3 提取嘴唇区域 将会使用到
- train_val_data:放置了本训练营为各位读者准备的数据集,将会在下一个 Task 用到
- test_img:放置了包含 4 种表情的图片各一张,将会在下一个 Task 用到,我们就是使用这个文件夹里的图片来展示我们的成果
3.1 图片格式统一
以下代码可以实现对图片格式的统一,我们在 img_type_test 文件下放置了几张测试图片,读者可以运行尝试。『读者也可以上传不同格式(如 jpg、jpeg、png 等)的图片到 img_type_test 文件下,然后运行下面的代码,下面的代码读取每种图片,然后将其转为 jpg 格式,同时会将原始的图片删除』
import os
import sys
import cv2
import numpy as np
def listfiles(rootDir):
list_dirs = os.walk(rootDir)
for root, dirs, files in list_dirs: # 遍历文件夹下的图片
for d in dirs:
print((os.path.join(root, d)))
for f in files:
fileid = f.split('.')[0] # 获得图片的名字,不含后缀
filepath = os.path.join(root, f)
print(filepath)
try:
src = cv2.imread(filepath, 1) # 读取原始图片,数据会加载到内存中
print("src=", filepath, src.shape)
os.remove(filepath) # 移除原来的图片
cv2.imwrite(os.path.join(root, fileid + ".jpg"), src) # 保存经过格式转换的图片
except:
os.remove(filepath)
continue
path = "./data/Emotion_Recognition_File/img_type_test/" # 输入图片路径即可,可以在这个文件夹下放置各种后缀名的图片,代码会将所有图片统一成 jpg 格式
listfiles(path)
./data/Emotion_Recognition_File/img_type_test/Baidu_0026.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0026.jpeg (394, 700, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0047.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0047.jpeg (431, 419, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0048.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0048.jpeg (2130, 1276, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0051.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0051.jpeg (640, 640, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0061.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0061.jpeg (1280, 2031, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0088.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0088.jpeg (853, 640, 3)
./data/Emotion_Recognition_File/img_type_test/Baidu_0093.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Baidu_0093.jpeg (634, 520, 3)
./data/Emotion_Recognition_File/img_type_test/Bing_0072.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Bing_0072.jpeg (728, 485, 3)
./data/Emotion_Recognition_File/img_type_test/Bing_0077.jpeg
src= ./data/Emotion_Recognition_File/img_type_test/Bing_0077.jpeg (941, 1079, 3)
统⼀格式为jpg之后预览如下:
3.2 数据清洗
利⽤搜索引擎爬取得到的图⽚肯定有不符合要求的,数据清洗主要是删除不合适的图⽚,即⾮⼈脸的照⽚。
可以采⽤⾁眼观察的⽅式,也可以利⽤程序进⾏筛选,我们调⽤ OpenCV 的⼈脸检测算法进⾏筛选,代码如下:
# coding:utf8
import cv2
import dlib
import numpy as np
import sys
import os
import matplotlib.pyplot as plt
# 人脸检测的接口,这个是 OpenCV 中自带的
cascade_path = './data/Emotion_Recognition_File/face_detect_model/haarcascade_frontalface_default.xml'
cascade = cv2.CascadeClassifier(cascade_path)
img_path = "./data/Emotion_Recognition_File/face_det_img/" # 测试图片路径
images = os.listdir(img_path)
for image in images:
im = cv2.imread(os.path.join(img_path, image), 1) # 读取图片
rects = cascade.detectMultiScale(im, 1.3, 5) # 人脸检测函数
print("检测到人脸的数量", len(rects))
if len(rects) == 0: # len(rects) 是检测人脸的数量,如果没有检测到人脸的话,会显示出图片,适合本地调试使用,在服务器上可能不会显示
cv2.namedWindow('Result', 0)
cv2.imshow('Result', im)
print("没有检测到人脸")
plt.imshow(im[:, :, ::-1]) # 显示
plt.show()
os.remove(os.path.join(img_path, image))
cv2.destroyAllWindows()
检测到人脸的数量 0
没有检测到人脸
检测到人脸的数量 1
检测到人脸的数量 1
检测到人脸的数量 1
检测到人脸的数量 0
没有检测到人脸
检测到人脸的数量 1
检测到人脸的数量 0
没有检测到人脸
检测到人脸的数量 1
检测到人脸的数量 1
这个⼈脸检测算法是传统算法,召回率不⾼,因此会有⼀些好样本被删除。经过处理后,可以看到都是⽐较好的样本了,后⾯提取⼈脸关键点也会简单很多。
如果利⽤⼈脸检测算法仍然⽆法清除⼲净样本,则需要⼿动筛选。当然如果你使⽤多个关键词或者使⽤不同的搜索引擎同样的关键词,或者从视频中提取图⽚,那么爬取回来的图⽚很可能有重复或者⾮常的相似,这样的数据集需要去重。
3.3 提取嘴唇区域
接下来我们要将样本处理成我们真正训练所需要的图像,本任务只对嘴唇部分的表情进⾏识别,所以我们的目标就是获取人脸嘴唇区域的图像,然后进行分类。我们利⽤ Opencv+Dlib 算法提取嘴唇区域, Dlib 算法会得到⾯部的 68 个关键点,我们从中得到嘴唇区域,并适当扩⼤。
人脸 68 点位置图如下:
下面的代码可以对图片进行人脸检测,检测到人脸后,会将嘴巴区域分割出来,形成数据集!
读者可以尝试运行代码,便可以理解这样做的意义
# coding:utf8
import cv2
import dlib
import numpy as np
import sys
import os
import matplotlib.pyplot as plt
# 配置 Dlib 关键点检测路径
# 文件可以从 http://dlib.net/files/ 下载
PREDICTOR_PATH = "./data/Emotion_Recognition_File/face_detect_model/shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(PREDICTOR_PATH)
# 配置人脸检测器路径
cascade_path = './data/Emotion_Recognition_File/face_detect_model/haarcascade_frontalface_default.xml'
cascade = cv2.CascadeClassifier(cascade_path)
# 调用 cascade.detectMultiScale 人脸检测器和 Dlib 的关键点检测算法 predictor 获得关键点结果
def get_landmarks(im):
rects = cascade.detectMultiScale(im, 1.3, 5) # 人脸检测
x, y, w, h = rects[0] # 获取人脸的四个属性值,左上角坐标 x,y 、高宽 w、h
# print(x, y, w, h)
rect = dlib.rectangle(int(x), int(y), int(x + w), int(y + h))
return np.matrix([[p.x, p.y] for p in predictor(im, rect).parts()])
def annotate_landmarks(im, landmarks):
im = im.copy()
for idx, point in enumerate(landmarks):
pos = (point[0, 0], point[0, 1])
cv2.putText(im,
str(idx),
pos,
fontFace=cv2.FONT_HERSHEY_SCRIPT_SIMPLEX,
fontScale=0.4,
color=(0, 0, 255))
cv2.circle(im, pos, 5, color=(0, 255, 255))
return im
def getlipfromimage(im, landmarks):
xmin = 10000
xmax = 0
ymin = 10000
ymax = 0
# 根据最外围的关键点获取包围嘴唇的最小矩形框
# 68 个关键点是从
# 左耳朵0 -下巴-右耳朵16-左眉毛(17-21)-右眉毛(22-26)-左眼睛(36-41)
# 右眼睛(42-47)-鼻子从上到下(27-30)-鼻孔(31-35)
# 嘴巴外轮廓(48-59)嘴巴内轮廓(60-67)
for i in range(48, 67):
x = landmarks[i, 0]
y = landmarks[i, 1]
if x < xmin:
xmin = x
if x > xmax:
xmax = x
if y < ymin:
ymin = y
if y > ymax:
ymax = y
print("xmin=", xmin)
print("xmax=", xmax)
print("ymin=", ymin)
print("ymax=", ymax)
roiwidth = xmax - xmin
roiheight = ymax - ymin
roi = im[ymin:ymax, xmin:xmax, 0:3]
if roiwidth > roiheight:
dstlen = 1.5 * roiwidth
else:
dstlen = 1.5 * roiheight
diff_xlen = dstlen - roiwidth
diff_ylen = dstlen - roiheight
newx = xmin
newy = ymin
imagerows, imagecols, channel = im.shape
if newx >= diff_xlen / 2 and newx + roiwidth + diff_xlen / 2 < imagecols:
newx = newx - diff_xlen / 2
elif newx < diff_xlen / 2:
newx = 0
else:
newx = imagecols - dstlen
if newy >= diff_ylen / 2 and newy + roiheight + diff_ylen / 2 < imagerows:
newy = newy - diff_ylen / 2
elif newy < diff_ylen / 2:
newy = 0
else:
newy = imagerows - dstlen
roi = im[int(newy):int(newy + dstlen), int(newx):int(newx + dstlen), 0:3]
return roi
def listfiles(rootDir):
list_dirs = os.walk(rootDir)
for root, dirs, files in list_dirs:
for d in dirs:
print(os.path.join(root, d))
for f in files:
fileid = f.split('.')[0]
filepath = os.path.join(root, f)
try:
im = cv2.imread(filepath, 1)
landmarks = get_landmarks(im)
roi = getlipfromimage(im, landmarks)
roipath = filepath.replace('.jpg', '_mouth.png')
cv2.imwrite(roipath, roi)
plt.imshow(roi[:, :, ::-1])
plt.show()
except:
print("error")
continue
listfiles("./data/Emotion_Recognition_File/mouth_det_img/")
xmin= 194
xmax= 217
ymin= 141
ymax= 155
xmin= 551
xmax= 723
ymin= 1150
ymax= 1235
xmin= 302
xmax= 351
ymin= 327
ymax= 367
xmin= 279
xmax= 368
ymin= 295
ymax= 349
xmin= 200
xmax= 280
ymin= 387
ymax= 444
xmin= 517
xmax= 612
ymin= 495
ymax= 520
运行上面的代码会显示嘴巴区域图,如果看不到,重新运行一下即可
(xmin,ymin) (xmax,ymax) 分别代表嘴唇区域在原始图像的位置,即左上角坐标和右下角坐标
结果如下,⾃此就完成了⾃建数据集操作
上面就是本训练营『数据获取与整理』部分的内容,有了数据,接下来的任务就可以训练我们的表情分类模型吧。
加油!这也许是你的第一个深度学习模型了。