【机器学习】基于 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 退出。

相关推荐
TaoMetrix11 小时前
TaoMetrix:AI 硬件创业公司如何做好原型制造
人工智能·制造
青 春 记 忆11 小时前
零基础入门python30:Flask个人账本从空目录运行与阶段验收
python·flask·后端开发
智圣新创0111 小时前
面向多级组织协同场景 高校第二课堂一站式管理中枢落地全场景实操答疑
大数据·人工智能
cd_9492172111 小时前
AI纹理和Substance Painter手绘纹理哪个更适合游戏资产制作?
人工智能·游戏·substance painter
sali-tec11 小时前
C# 基于OpenCv的视觉工作流-章106-差值追踪
图像处理·人工智能·opencv·算法·计算机视觉
Shockang11 小时前
用 AI 打造高品质 Web 应用
人工智能
l12586512 小时前
# LangGraph Memory机制深度解析:短期记忆与长期记忆的工程实践
前端·人工智能·python·langchain·bootstrap
手写码匠12 小时前
Dify 多 Agent 工具权限与安全沙箱实战:让智能体“有能力,但不越权“
人工智能·深度学习·算法·aigc
ZGIAI12 小时前
ZGI Skill Loop:给工具调用设边界
人工智能·架构
黎阳之光12 小时前
打破堆场感知黑盒:黎阳之光视频孪生,构建港口码头网格化透明管控新体系
大数据·人工智能·算法·安全·数字孪生