文章目录
- 完整代码一览
- 一、环境准备与模型文件
- 二、核心原理:三个关键指标
-
- [1. EAR(眼睛纵横比)------ 判断睁眼/闭眼](#1. EAR(眼睛纵横比)—— 判断睁眼/闭眼)
- [2. MAR(嘴巴纵横比)------ 判断嘴巴张开程度](#2. MAR(嘴巴纵横比)—— 判断嘴巴张开程度)
- [3. MJR(嘴宽脸宽比)------ 判断嘴巴拉宽程度](#3. MJR(嘴宽脸宽比)—— 判断嘴巴拉宽程度)
- 三、逐行代码详解
-
- [1. 导入库与定义指标函数](#1. 导入库与定义指标函数)
- [MAR 函数](#MAR 函数)
- [MJR 函数](#MJR 函数)
- 辅助函数
-
- [中文绘制 cv2AddChineseText](#中文绘制 cv2AddChineseText)
- [绘制眼睛轮廓 drawEye](#绘制眼睛轮廓 drawEye)
- [3. 主程序结构](#3. 主程序结构)
- [4. 主循环](#4. 主循环)
- 绘制与显示
完整代码一览
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 退出。