深度学习(四)——dlib+skimage实时识别人脸夸张表情

一、背景

这是两年前在公司做的小项目,后来公司倒闭了,就把这部分内容分享出来。相关工作应该是在18年的9-11月份之间做的。

二、算法思路

最开始想的思路是利用深度学习来做,具体做法也可以参考深度学习(二)——从零自己制作数据集到利用deepNN实现夸张人脸表情的实时监测(tensorflow实现),但是最大问题有两个:(1)如果你想要一个效果比较好的网络,那很大程度上需要更深层的网络,这就导致训练好的checkpoint或者pb文件过大,很难直接应用到移动设备上。(2)训练数据的好坏直接影响表情识别的效果,而我也没有海量的训练数据,所有的数据都是自己从网上爬取的,所以最终的训练效果不太理想。

后来又有了更简单的想法,因为dlib库有一个可以检测人脸特征点的模型,因此可以考虑用dlib库识别出人脸的关键特征点,然后利用这些特征点来计算一些表情特征。同时,图像处理主要用skimage库,主要用于裁剪识别出的人脸。最后再通过一些阈值来筛选计算的特征值,来判断人脸的表情。

可以这么做的原因是因为人脸特征点的对应位置编号,在不同影像中都是一样的,因此这就为人脸特征点识别带来很大方便。人脸特征点的位置如下图:

将这张图用于不同的人脸,可以看到每个位置的特征点编号都是一致的:

三、实现过程

1.文件结构

文件比较少,所有的文件结构如下:

-- shape_predictor_68_face_landmarks.dat    # 68点人脸特征检测模型
-- face_analysis.py                         # 识别人脸特征,计算关键参数
-- video_camera_image_test.py               # 调用视频,摄像头,图像进行测试  

2.数据准备

关于数据准备,其实只要自己准备一个68点人脸特征检测模型就可以了。shape_predictor_68_face_landmarks.dat文件网上也比较多,这里直接给出一个下载地址:https://github.com/AKSHAYUBHAT/TensorFace/blob/master/openface/models/dlib/shape_predictor_68_face_landmarks.dat

打开上面的链接,点击下面的download,下载好之后把该文件放到project的根目录下即可:

3. 识别计算人脸特征参数文件文件face_analysis.py

这一步的主要思路是设计类,类中传入人脸识别的关键特征点,然后在类中设计算法计算特征值,比如眼睛的高度,和两眼的高度差,可以来判别是否闭眼或眨眼等特征。可以计算的特征值很多,我这里主要考虑了鼻子(判断人脸偏向),眼睛(眨眼或闭眼),嘴巴(张嘴,吃惊,or闭嘴),鼻尖和鼻翼(抬头or低头),暂时想到的就这些,然后通过计算实验,设计合理阈值。一下是该部分的代码:

import cv2
import dlib
from skimage import io
import time


class EmotionPara:
    # 类的初始化,即传入参数仅有切割好的人脸图像和特征提取器
    def __init__(self, face_img, shape, d):
        self.img = face_img         # 存储了人脸的图像
        self.shape = shape          # 存储了人脸特征点的图像坐标
        self.d = d                  # 表示第d个人脸的框位置

        self.face_width = (self.shape.part(15).x - self.shape.part(1).x)    # 人脸的宽度
        self.face_height = (self.d.bottom() - self.d.top())                 # 人脸的高度
        self.proof_brow_width = self.proof_brow_width()                     # 人脸方位的校正系数

        self.nose = self.nose_para()                        # 人脸的方位向左还是向右
        self.mouth = self.mouth_para()                      # 判断嘴是否张着,是轻微张嘴还是大张
        self.faceupdown = self.faceupdown_para()            # 判断人脸是向上还是向下
        self.eye = self.eye_para()                          # 判断眼睛是否睁开,是否眨眼
        self.brow = self.brow_para()                        # 眉毛参数

        print(self.nose,
              self.mouth,
              self.faceupdown,
              self.eye,
              self.brow)

    # 计算和嘴有关的参数
    def mouth_para(self):
        # 嘴的高度
        mouth_height = ((self.shape.part(63).y - self.shape.part(65).y) +
                        (self.shape.part(62).y - self.shape.part(66).y) +
                        (self.shape.part(61).y - self.shape.part(67).y)) / 3
        # 嘴的宽度
        mouth_width = ((self.shape.part(64).x - self.shape.part(48).x) +
                       (self.shape.part(54).x - self.shape.part(60).x)) / 2
        # 计算嘴的曲率
        mouth_curv = abs(mouth_height / mouth_width)
        # 计算嘴和鼻尖之间的距离,用以判断是抬头还是低头
        # mouth_to_nose = self.shape.part(33).y - self.shape.part(51).y

        mouth_height_ratio = abs(mouth_height / self.face_height)
        # mouth_width_ratio = abs(mouth_width / self.face_width)
        # mouth_to_nose_ratio = abs(mouth_to_nose / self.face_height)

        # 判断是否张嘴,并简单判断嘴的表情
        if mouth_height_ratio > 0.045:  # 张嘴
            if mouth_curv > 0.35:
                mouth = "张大嘴巴,非常吃惊"
            elif mouth_curv > 0.18:
                mouth = "张嘴,有点小激动"
            else:
                mouth = "轻微张嘴"
        else:  # 闭嘴
            mouth = "闭嘴"

        return mouth

        # return mouth_height_ratio, mouth_width_ratio, mouth_curv, mouth_to_nose_ratio

    # 计算和眼睛有关的参数,返回左眼和右眼的高度,以及左右眼的高度差
    def eye_para(self):
        # 左眼的张开高度
        eye_left_height = ((self.shape.part(37).y + self.shape.part(38).y) - (
                self.shape.part(40).y + self.shape.part(41).y)) / 2
        # 右眼的张开高度
        eye_right_height = ((self.shape.part(43).y + self.shape.part(44).y) - (
                self.shape.part(46).y + self.shape.part(47).y)) / 2

        eye_left_height_ratio = abs(eye_left_height / self.face_height)
        eye_right_height_ratio = abs(eye_right_height / self.face_height)

        # 左右眼的张开高度差
        diff_left_right = abs(eye_left_height_ratio - eye_right_height_ratio) * 10

        # 然后判断眼睛的情况
        # eye_left, eye_right, diff = face_para.eye_para()
        # print("eye: ", face_para.eye_para())
        if eye_left_height_ratio > 0.034 and eye_right_height_ratio > 0.034:  # 睁眼
            if eye_left_height_ratio > 0.07 and eye_right_height_ratio > 0.07 and diff_left_right < 0.060:
                eye = "睁大眼睛"
            elif diff_left_right < 0.060:
                eye = "睁眼"
            else:
                if (eye_left_height_ratio - eye_right_height_ratio) > 0:
                    eye = "眨右眼"
                else:
                    eye = "眨左眼"
        elif diff_left_right >= 0.060:
            if (eye_left_height_ratio - eye_right_height_ratio) > 0:
                eye = "眨右眼"
            else:
                eye = "眨左眼"
        else:
            eye = "闭眼"

        return eye

    # 计算和眉毛有关的参数
    def brow_para(self):
        brow_height_sum = 0         # 高度之和
        brow_width_sum = 0          # 左边眉毛距离之和
        line_brow_x = []            # 存储左眉毛的x坐标
        line_brow_y = []            # 存储左眉毛的y坐标
        brow_height_ratio = 0
        brow_width_ratio = 0

        for j in range(17, 21):     # 左边眉毛的编号是17到21, 右眉毛的编号是22到26,即j+5
            brow_height_sum += (self.shape.part(j).y - self.d.top()) + (self.shape.part(j + 5).y - self.d.top())
            brow_width_sum += (self.shape.part(j + 5).x - self.shape.part(j).x)
            line_brow_x.append(self.shape.part(j).x)
            line_brow_y.append(self.shape.part(j).y)
            # print(brow_height_sum, brow_width_sum)
            brow_height_ratio += (brow_height_sum / 10) / self.face_height    # 眉毛高度占比
            brow_width_ratio += (brow_width_sum / 5) / self.face_width       # 眉毛距离占比
            # print(brow_height_ratio, brow_width_ratio)

        # 最后判断是否皱眉
        # brow_height, brow_width = face_para.brow_para()
        # print(brow_width / proof_brow_width)
        if brow_width_ratio / self.proof_brow_width < 0.9:
            brow = "皱眉,眉头紧锁"
        else:
            brow = ""

        return brow

    # 计算鼻子的相关参数,主要判断鼻子在人脸的中间还是两侧
    def nose_para(self):
        nose_location = self.shape.part(29).x - self.shape.part(1).x
        nose_location_ratio = nose_location / self.face_width

        if nose_location_ratio < 0.4:
            nose = "向左转"
        elif nose_location_ratio > 0.65:
            nose = "向右转"
        else:
            nose = "正脸"
        return nose

    # 人脸方位的校正系数
    def proof_brow_width(self):
        nose_location = self.shape.part(29).x - self.shape.part(1).x
        nose_location_ratio = nose_location / self.face_width
        proof_brow_width = 1 - abs(nose_location_ratio - 0.5)
        return proof_brow_width

    # 计算能够判断人脸向上还是向下的参数
    def faceupdown_para(self):
        nose_tip = self.shape.part(30).y
        alar = (self.shape.part(31).y + self.shape.part(35).y) / 2
        uporlower_head = (alar - nose_tip) / self.face_height

        # 根据鼻尖到嘴唇的距离,判断人脸向上或向下看
        # uporlower_head = face_para.faceupdown_para()
        # print("lips: ", uporlower_head)
        if uporlower_head > 0.10:
            face_updown = "抬头"
        elif uporlower_head < 0.034:
            face_updown = "低头"
        else:
            face_updown = ""

        return face_updown

    # # 利用嘴角判断高兴还是悲伤
    # def mouthemo_para(self):
    #     up_lip = (self.shape.part(48).y + self.shape.part(54).y) / 2
    #     down_lip = (self.shape.part(67).y + self.shape.part(65).y) / 2
    #     emo_ratio = (up_lip - down_lip) / self.face_height
    #     return emo_ratio


def emotion_analysis(img, detector, predictor, show_img=False):
    dets = detector(img, 1)

    for k, d in enumerate(dets):

        width = d.right() - d.left()
        height = d.bottom() - d.top()

        face_img = img[d.left():d.left() + width, d.top():d.top() + height]
        shape = predictor(img, d)
        EmotionPara(face_img, shape, d)

        if show_img:
            #    利用预测器预测
            shape = predictor(img, d)
            # # 标出68个点的位置-------------------------------------------------
            for i in range(68):
                cv2.circle(img, (shape.part(i).x, shape.part(i).y), 2, (0, 255, 0), -1, 4)
                cv2.putText(img, str(i), (shape.part(i).x, shape.part(i).y),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255))
            # # # -----------------------------------------------------------------
            # # # 显示一下处理的图片,然后销毁窗口
            b, g, r = cv2.split(img)
            img2 = cv2.merge([r, g, b])
            cv2.imshow('face', img2)
            cv2.waitKey(0)


if __name__ == '__main__':
    time_start = time.time()
    detector = dlib.get_frontal_face_detector()
    predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
    time_mid = time.time()
    img = io.imread('./test/000001.png')
    emotion_analysis(img, detector, predictor)
    time_end = time.time()
    print('totally cost', time_end - time_mid)

给出算法(2)的代码:

from one_face_recog import *
# import cv2
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")


def video_emotion(video_dir):
    '''
    功能介绍:传入视频,分析视频中的表情
    '''

    vc = cv2.VideoCapture(video_dir)  # 读入视频文件

    if vc.isOpened():  # 判断是否正常打开
        rval, frame = vc.read()
    else:
        rval = False

    timeF = 100  # 视频帧计数间隔频率

    c = 1
    while (vc.isOpened()):  # 循环读取视频帧
        rval, frame = vc.read()
        if (c%timeF == 0):  # 每隔timeF帧进行存储操作
            cv2.imwrite('image/' + str(c) + '.jpg', frame)  # 存储为图像

        # emotion_analysis(frame)
        emotion_analysis('image/' + str(c) + '.jpg', detector, predictor)
        c = c + 1
        # cv2.waitKey(1)
    vc.release()


def camera_emotion():
    '''
    功能介绍:直接打开摄像头,进行实时的表情分析
    '''

    # 打开摄像头
    video_captor = cv2.VideoCapture(0)      # 创建一个 VideoCapture 对象

    timef = 10  # 视频帧计数间隔频率
    c = 1

    while True:
        # 打开摄像头并做准备
        ret_flag, frame = video_captor.read()
        cv2.imshow("Capture_Test", frame)
        if c % timef == 0:
            emotion_analysis(frame, detector, predictor)
        c += 1
        k = cv2.waitKey(1)
        if k == ord('q'):  # 若检测到按键 ‘s’,打印字符串
            break
    video_captor.release()  # 释放摄像头
    cv2.destroyAllWindows()  # 删除建立的全部窗口


def txt_emotion(txt_dir):
    """
    功能介绍:打开txt中的文件批量处理
    """
    time_start = time.time()
    fopen = open(txt_dir, 'r')
    lines = fopen.read().splitlines()  # 逐行读取txt
    # lines = random_shuffle(lines)
    count = len(open(txt_dir, 'rU').readlines())  # 计算txt有多少行
    # print(count)

    # time_start = time.time()
    i = 1
    # print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
    for line in lines:
        line = line.split(" ")  # 利用空格进行分割

        img = io.imread(line[0])
        # emotion_analysis(img)
        emotion_analysis(img, detector, predictor)

        i += 1
    time_end = time.time()
    print('totally cost', time_end - time_start)
    print("the %d image has been processed" % (i))


def main(using_cv=False, using_txt=True, txt_dir='', using_video=False, video_dir=""):
    if using_cv:
        camera_emotion()
    elif using_txt:
        txt_emotion(txt_dir)
    elif using_video:
        video_emotion(video_dir)
    else:
        img = io.imread('D:/python/item/26-facedance(finished)/pre-data/zhayan/230.jpg')
        emotion_analysis(img, detector, predictor)


if __name__ == '__main__':
    main(using_cv=True,
         using_txt=False, txt_dir='./pre-data/test.txt',
         using_video=False, video_dir="")
    # test('D:/python/item/26-facedance(finished)/pre-data/test.txt')

四、实验效果

这里我随便找了一张图片来测试一下效果:

输出结果为(这里的时间是我查看程序的时间,不是程序处理的时间):

在来一张:

识别的表情结果:

五、分析

1. 我对大量图像进行了批量处理,平均下来大约一张图0.04s就可以识别出结果,也就是1秒25帧的样子,基本达到准实时识别的效果。

2. 对一些微表情,我觉得还有改进的空间。另外我还是觉得拿深度学习做的效果会比这种比参数的效果好很多,但是目前我没有比较好的数据集,后面有时间了再优化吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全部梭哈迟早暴富

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值