OpenCV 实战:基于人脸关键点的微笑与大笑识别(附完整代码)
从零开始理解嘴部纵横比(MAR)和嘴下颌宽比(MJR),轻松实现表情识别
一、前言
作为 OpenCV 的初学者,你是否想过用摄像头判断一个人是在微笑还是在大笑?本文带你用dlib获取人脸 68 个关键点,然后通过简单的几何比例计算,实现表情识别。最终效果如下:
二、实现原理
2.1 人脸关键点检测
我们使用 dlib 提供的68 点面部关键点模型,它能标出眉毛、眼睛、鼻子、嘴巴和下颌轮廓的位置。
其中嘴巴周围的关键点编号如下:
48 ~ 60:外嘴唇轮廓
61 ~ 67:内嘴唇轮廓
2.2 嘴部开合程度 —— MAR
MAR(Mouth Aspect Ratio,嘴部纵横比)用来量化嘴巴张开的程度。
公式:
MAR=(A+B+C)/3DMAR=D(A+B+C)/3
A:上嘴唇中点(50)与下嘴唇中点(58)的距离
B:上嘴唇左侧内沿(51)与下嘴唇左侧内沿(57)的距离
C:上嘴唇右侧内沿(52)与下嘴唇右侧内沿(56)的距离
D:左右嘴角(48 和 54)的水平距离
当嘴巴张大时,MAR 会明显增大。我们设置阈值0.5,超过即为“大笑”。
2.3 微笑程度 —— MJR
MJR(Mouth-to-Jaw Ratio,嘴宽与下颌宽之比)用于判断微笑。
公式:
MJR=MJMJR=JM
M:左右嘴角(48 和 54)的距离
J:两侧下颌角(3 和 13)的距离
微笑时嘴角向两侧拉伸,M 增大,而 J 基本不变,因此 MJR 变大。阈值0.45以上且未达到大笑标准时,判定为“微笑”。
2.4 判断逻辑
if mar > 0.5: result = "大笑" elif mjr > 0.45: result = "微笑" else: result = "正常"三、完整代码及逐行注释
import numpy as np import dlib import cv2 from sklearn.metrics.pairwise import euclidean_distances from PIL import Image, ImageDraw, ImageFont def MAR(shape): """计算嘴部纵横比""" A = euclidean_distances(shape[50].reshape(1,2), shape[58].reshape(1,2))[0][0] B = euclidean_distances(shape[51].reshape(1,2), shape[57].reshape(1,2))[0][0] C = euclidean_distances(shape[52].reshape(1,2), shape[56].reshape(1,2))[0][0] D = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))[0][0] return ((A+B+C)/3) / D def MJR(shape): """计算嘴宽与下颌宽之比""" M = euclidean_distances(shape[48].reshape(1,2), shape[54].reshape(1,2))[0][0] J = euclidean_distances(shape[3].reshape(1,2), shape[13].reshape(1,2))[0][0] return M / J def cv2AddChineseText(img, text, position, textColor=(0,255,0), textSize=30): """在 OpenCV 图片上绘制中文(因为 cv2.putText 不支持中文)""" img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_pil) font = ImageFont.truetype("simsun.ttc", textSize, encoding="utf-8") draw.text(position, text, textColor, font=font) return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) # ---------- 初始化 ---------- detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # ---------- 读取图片 ---------- img_path = "test.jpg" # 改成你的图片路径 frame = cv2.imread(img_path) if frame is None: print("图片读取失败,请检查路径") exit() # ---------- 人脸检测与关键点提取 ---------- faces = detector(frame, 0) for face in faces: shape = predictor(frame, face) shape = np.array([[p.x, p.y] for p in shape.parts()]) mar = MAR(shape) mjr = MJR(shape) # 表情分类 result = "正常" if mar > 0.5: result = "大笑" elif mjr > 0.45: result = "微笑" # 绘制嘴部轮廓和文字 mouthHull = cv2.convexHull(shape[48:61]) text_pos = (int(mouthHull[0][0][0]), int(mouthHull[0][0][1] - 20)) frame = cv2AddChineseText(frame, result, text_pos, textSize=30) cv2.drawContours(frame, [mouthHull], -1, (0,255,0), 1) # ---------- 显示结果 ---------- cv2.imshow("result", frame) cv2.waitKey(0) cv2.destroyAllWindows()注意:需要提前下载
shape_predictor_68_face_landmarks.dat文件,并安装依赖:pip install opencv-python dlib scikit-learn pillow numpy
四、知识点扩展
4.1 欧氏距离的多种写法
代码中的euclidean_distances(a,b)[0][0]其实是计算两点之间的直线距离。对于初学者,更推荐使用 NumPy 自带的写法:
# 方法一(最简洁) dist = np.linalg.norm(shape[50] - shape[58]) # 方法二(手动公式) dist = np.sqrt((shape[50][0]-shape[58][0])**2 + (shape[50][1]-shape[58][1])**2)4.2 为什么使用比例而不是绝对值?
因为人脸距离摄像头的远近会影响绝对距离值,而比例(如 MAR、MJR)具有尺度不变性,无论人脸大小如何,比例都能稳定反映表情特征。
4.3 如何确定阈值?
代码中使用的mar > 0.5和mjr > 0.45是经验值。实际应用中,你可以收集几十张自己不同表情的照片,计算各自的 MAR 和 MJR,画出分布直方图,再取合适的临界值。
五、改进与扩展:加入眼睛判断
目前的算法只依赖嘴部特征,无法区分“大笑”和“闭眼大笑”(可能是假笑或疲劳)。为了更精准,可以引入眼睛纵横比(EAR)来判断眼睛的闭合程度。
EAR 公式(与 MAR 类似):
EAR=∥p37−p41∥+∥p38−p40∥2⋅∥p36−p39∥EAR=2⋅∥p36−p39∥∥p37−p41∥+∥p38−p40∥
其中 36~41 是左眼的六个关键点。
你可以这样扩展逻辑:
如果
mar > 0.5且ear < 0.25,则可能是在假笑或疲劳如果
mar > 0.5且ear > 0.3,才是真正的大笑
(此处插入眼睛关键点编号图)
有兴趣的读者可以自行实现这个增强版。
六、运行常见问题
| 问题 | 解决方案 |
|---|---|
dlib.shape_predictor加载失败 | 检查.dat文件路径是否正确,建议使用绝对路径 |
| 中文显示为方框 | 把simsun.ttc换成你系统里存在的中文字体,例如msyh.ttc(微软雅黑) |
| 图片读取失败 | 确保图片路径不含中文,且文件确实存在 |
| 检测不到人脸 | 尝试换一张正面、光线均匀的照片 |
七、总结
通过这篇笔记,你学会了:
使用 dlib 检测人脸和 68 个关键点
计算 MAR 和 MJR 两个几何特征
基于简单阈值实现微笑 / 大笑 / 正常分类
在 OpenCV 图片上绘制中文
了解到眼睛纵横比(EAR)作为进一步的优化方向
人脸表情识别是一个很有趣的入门项目,你可以把它扩展到实时摄像头、疲劳驾驶检测、情绪分析等场景。希望这篇笔记对你有帮助!
源码与模型下载:文中代码可以直接复制使用,
shape_predictor_68_face_landmarks.dat可以在 dlib 官网或 GitHub 上找到。
本文为作者学习 OpenCV 过程中的笔记,欢迎交流讨论。
