【机器学习】基于 dlib 面部关键点的多表情分类

文章目录


完整代码一览

c 复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont

# MAR:嘴巴纵横比
def MAR(shape):
    A = euclidean_distances(shape[50].reshape(1,2), shape[58].reshape(1,2))
    B = euclidean_distances(shape[51].reshape(1,2), shape[57].reshape(1,2))
    C = euclidean_distances(shape[52].reshape(1,2), shape[56].reshape(1,2))
    D = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))
    return ((A+B+C)/3) / D

# EAR:眼睛纵横比
def eye_aspect_ratio(eye):
    A = euclidean_distances(eye[1].reshape(1,2), eye[5].reshape(1,2))
    B = euclidean_distances(eye[2].reshape(1,2), eye[4].reshape(1,2))
    C = euclidean_distances(eye[0].reshape(1,2), eye[3].reshape(1,2))
    ear = ((A + B) / 2.0) / C
    return ear

# MJR:嘴宽 / 脸宽比值
def MJR(shape):
    M = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))
    J = euclidean_distances(shape[3].reshape(1,2), shape[13].reshape(1,2))
    return M / J

# OpenCV 中文绘制
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=30):
    if isinstance(img, np.ndarray):
        img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img)
    font = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
    draw.text(position, text, textColor, font=font)
    return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

def drawEye(eye):
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 2)

detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
cap = cv2.VideoCapture(0)
COUNTER = 0

while True:
    ret, frame = cap.read()
    if not ret:
        break
    faces = detector(frame, 0)
    for face in faces:
        shape = predictor(frame, face)
        shape = np.array([[p.x, p.y] for p in shape.parts()])
        rightEye = shape[36:42]
        leftEye = shape[42:48]
        rightEAR = eye_aspect_ratio(rightEye)
        leftEAR = eye_aspect_ratio(leftEye)
        ear = (leftEAR + rightEAR) / 2.0
        mar = MAR(shape)
        mjr = MJR(shape)
        print('mar', mar, '\tmjr', mjr)

        result = "正常"
        if mar > 0.5:
            result = "大笑"
        elif mjr > 0.45:
            result = "微笑"
        elif mar > 0.45 and ear < 0.3:
            COUNTER += 1
            if COUNTER >= 30:
                result = '哭'
        elif ear > 0.35:
            result = '愤怒'
        else:
            COUNTER = 0

        drawEye(leftEye)
        drawEye(rightEye)
        info = "EAR: {:.2f}".format(ear[0][0])
        frame = cv2AddChineseText(frame, info, (0,30))
        frame = cv2AddChineseText(frame, result, (50, 100))
        mouthHull = cv2.convexHull(shape[48:61])
        cv2.drawContours(frame, [mouthHull], -1, (0,255,0), 1)

    cv2.imshow("Frame", frame)
    if cv2.waitKey(1) == 27:
        break

cap.release()
cv2.destroyAllWindows()

一、环境准备与模型文件

在运行代码之前,请确保已安装以下库:

c 复制代码
pip install dlib opencv-python scikit-learn Pillow

并下载 dlib 的 68 点关键点模型文件:

下载地址

解压后得到 shape_predictor_68_face_landmarks.dat,放在脚本同一目录。

中文绘制需要字体文件,本代码使用 simhei.ttf(黑体),你可以从 Windows 系统字体文件夹(C:\Windows\Fonts)复制,或使用其他中文字体(如 msyh.ttc),并修改代码中的字体名称。

二、核心原理:三个关键指标

1. EAR(眼睛纵横比)------ 判断睁眼/闭眼

我们在上一篇文章中已经详细介绍过,它基于眼睛 6 个点:

睁眼时 EAR ≈ 0.25~0.3,闭眼时接近 0。

用于区分"愤怒"(眼睛睁圆,EAR 较大)和"哭"(眼睛闭合,EAR 很小)。

2. MAR(嘴巴纵横比)------ 判断嘴巴张开程度

MAR 是 Mouth Aspect Ratio 的缩写,基于嘴巴 6 个点(内唇上下)和宽度:

嘴巴张开(大笑、惊讶)时,MAR 明显增大(> 0.5)。

嘴巴闭合时,MAR 较小(< 0.4)。

3. MJR(嘴宽脸宽比)------ 判断嘴巴拉宽程度

MJR 是 Mouth-to-Jaw Ratio,即嘴巴宽度与下颌宽度(脸部宽度)的比值:

微笑时,嘴巴会向两侧拉宽,MJR 增大(> 0.45)。

正常表情时,MJR 较小。

通过这三个指标的组合,我们可以区分多种表情。

三、逐行代码详解

1. 导入库与定义指标函数

c 复制代码
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances #提供欧氏距离计算
from PIL import Image, ImageDraw, ImageFont #中文绘制

MAR 函数

c 复制代码
def MAR(shape):
    A = euclidean_distances(shape[50].reshape(1,2), shape[58].reshape(1,2))
    B = euclidean_distances(shape[51].reshape(1,2), shape[57].reshape(1,2))
    C = euclidean_distances(shape[52].reshape(1,2), shape[56].reshape(1,2))
    D = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))
    return ((A+B+C)/3) / D

shape 是 68 个点的坐标数组,索引 48~60 是嘴巴区域。

50/58、51/57、52/56 是三对垂直方向上的内唇点,取平均得到嘴巴高度;48 和 54 是嘴角两端,代表嘴巴宽度。

MAR = 平均高度 / 宽度,张嘴时增大。

MJR 函数

c 复制代码
def MJR(shape):
    M = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))  # 嘴宽
    J = euclidean_distances(shape[3].reshape(1,2), shape[13].reshape(1,2))   # 下颌宽度
    return M / J

shape48 和 shape54 是嘴角,其距离即嘴宽。

shape3 和 shape13 是下巴轮廓两端的点(相当于脸部宽度)。

微笑时嘴巴变宽,比值增大。

辅助函数

中文绘制 cv2AddChineseText

c 复制代码
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=30):
    if isinstance(img, np.ndarray):
        img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img)
    font = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
    draw.text(position, text, textColor, font=font)
    return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

将 OpenCV 图像转为 PIL 图像,绘制文字后再转回 OpenCV BGR 格式。

绘制眼睛轮廓 drawEye

c 复制代码
def drawEye(eye):
    eyeHull = cv2.convexHull(eye)
    cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 2)

3. 主程序结构

c 复制代码
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
cap = cv2.VideoCapture(0)
COUNTER = 0

初始化检测器、预测器、摄像头,COUNTER 用于连续闭眼帧计数。

4. 主循环

c 复制代码
while True:
    ret, frame = cap.read()
    if not ret:
        break
    faces = detector(frame, 0)

逐帧读取,检测人脸。

提取关键点并计算指标

c 复制代码
    for face in faces:
        shape = predictor(frame, face)
        shape = np.array([[p.x, p.y] for p in shape.parts()])
        rightEye = shape[36:42]
        leftEye = shape[42:48]
        rightEAR = eye_aspect_ratio(rightEye)
        leftEAR = eye_aspect_ratio(leftEye)
        ear = (leftEAR + rightEAR) / 2.0
        mar = MAR(shape)
        mjr = MJR(shape)
        print('mar', mar, '\tmjr', mjr)

获取 68 点,截取左右眼和嘴巴,计算三个指标。print 输出到控制台便于调试。

表情判定逻辑

c 复制代码
        result = "正常"
        if mar > 0.5:
            result = "大笑"
        elif mjr > 0.45:
            result = "微笑"
        elif mar > 0.45 and ear < 0.3:
            COUNTER += 1
            if COUNTER >= 30:
                result = '哭'
        elif ear > 0.35:
            result = '愤怒'
        else:
            COUNTER = 0

采用级联判断:

如果 MAR > 0.5,判定为"大笑"(嘴巴张得很大)。

否则如果 MJR > 0.45,判定为"微笑"(嘴巴拉宽但未张很大)。

否则如果 MAR > 0.45 且 EAR < 0.3(嘴巴张开且眼睛闭合),可能是"哭",需要连续 30 帧确认(与疲劳检测类似)。

否则如果 EAR > 0.35(眼睛睁得很圆),判定为"愤怒"。

否则为"正常"。

绘制与显示

c 复制代码
        drawEye(leftEye)
        drawEye(rightEye)
        info = "EAR: {:.2f}".format(ear[0][0])
        frame = cv2AddChineseText(frame, info, (0,30))
        frame = cv2AddChineseText(frame, result, (50, 100))
        mouthHull = cv2.convexHull(shape[48:61])
        cv2.drawContours(frame, [mouthHull], -1, (0,255,0), 1)

绘制眼睛和嘴巴的凸包轮廓(绿色),显示 EAR 值和表情结果。

最后显示画面,按 ESC 退出。

相关推荐
CypressTel1 小时前
OpenAI推出ChatGPT Work:AI开始从“回答问题”走向“完成工作”——赛柏特AI快讯
人工智能·chatgpt
连涨- AI脑波英语1 小时前
教育机构做英语增项,如何用30天试点判断AI脑波英语是否适配?
人工智能
HyperAI超神经1 小时前
数据集汇总丨英伟达开源Nemotron系列数据集,超10T tokens+40M 条后训练样本,覆盖数学推理/代码生成/多语言对话
人工智能·大模型·数据集·nvidia·预训练·代码生成·监督微调
xcLeigh1 小时前
从搜索引擎到 AI 编程:开发者获取知识方式的范式转移
人工智能·ai·架构·ai编程·开发·范式转移
mpp0071 小时前
纯技术视角深度解读范凌 WAIC 访谈:AI技术栈三层跃迁、落地工程瓶颈与下一代底层模型路线
人工智能
zoneyung2 小时前
省经信厅、市经信局及服务型制造专家组到访中扬立库调研指导
人工智能·制造
WHS-_-20222 小时前
CL-SEC: Cross-Layer Semantic Error Correction Empowered byLanguage Models
人工智能·语言模型·php
夜影风2 小时前
智能体开发的“脚手架“:主流框架(LangChain、AutoGen等)选型指南
人工智能·langchain·ai agent
ShallWeL2 小时前
【机器学习】(20)—— 类别不平衡
人工智能·算法·机器学习