一、引言:从找到人脸到读懂表情
在之前的人脸检测中,我们学习了使用Haar级联分类器来检测人脸。虽然Haar能快速找到人脸的位置,但它存在一些局限:对侧脸、遮挡、光照变化不够鲁棒,检测精度也有待提升。
今天,我们将认识一个更强大的人脸处理库------Dlib 。它是一个现代C++工具库,在机器学习、计算机视觉领域应用广泛,尤其在人脸检测 和人脸关键点定位方面表现出色。
更重要的是,Dlib不仅能找到人脸,还能精确定位68个关键点 ------眼睛、眉毛、鼻子、嘴巴、脸廓的精确坐标。有了这些关键点,我们就能进一步分析表情:微笑、大笑、哭泣、愤怒......
本篇博客将结合代码示例,从人脸检测到关键点定位,再到表情识别,带领大家系统掌握Dlib在人脸分析中的核心应用。
二、Dlib人脸检测
2.1 什么是Dlib的HOG检测器
Dlib的人脸检测器基于HOG(方向梯度直方图)特征 + 线性分类器 + 图像金字塔 + 滑动窗口技术。
核心原理:
-
将图像划分为小单元格,计算每个单元格的梯度方向直方图
-
将相邻单元格的直方图组合成HOG特征
-
使用线性SVM分类器判断每个窗口是否包含人脸
-
通过图像金字塔处理不同尺度的人脸
相比OpenCV的Haar级联分类器,Dlib的HOG检测器:
-
检测效果更好,尤其对正面人脸
-
误检率更低
-
速度较慢(因为没有Haar的级联加速机制)
2.2 静态图片人脸检测
python
import cv2
import dlib
# 构造人脸位置检测器(HOG)
detector = dlib.get_frontal_face_detector()
# 读取图像
img = cv2.imread("people.png")
# 检测人脸
faces = detector(img, 1)
for face in faces:
# 获取人脸框的坐标
x1 = face.left()
y1 = face.top()
x2 = face.right()
y2 = face.bottom()
# 绘制人脸框
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow("result", img)
cv2.waitKey(0)
detector(image, n)参数说明:
| 参数 | 含义 |
|---|---|
image |
待检测图像 |
n |
上采样次数,0或1通常足够;值越大越能检测到小人脸,但速度越慢 |
返回值 :faces 是一个包含所有检测到的人脸框的列表,每个人脸框有 left()、top()、right()、bottom() 四个方法。
cv2.rectangle() 参数说明:
python
cv2.rectangle(img, pt1, pt2, color, thickness=1, lineType=cv2.LINE_8, shift=0) -> img
cv2.rectangle(img, rec, color, thickness=1, lineType=cv2.LINE_8, shift=0) -> img
| 参数 | 含义 |
|---|---|
img |
要绘制的图像,一般是 numpy.ndarray,BGR 或灰度图 |
pt1 |
矩形一个顶点,通常左上角,格式 (x, y) |
pt2 |
与 pt1 相对的顶点,通常右下角,格式 (x, y) |
rec |
矩形区域,格式 (x, y, w, h),即左上角坐标和宽高 |
color |
颜色。彩色图是 BGR 元组,灰度图直接传一个整数 |
thickness |
线宽,默认 1,-1 或 cv2.FILLED 表示填充整个矩形。 |
lineType |
线型,默认 cv2.LINE_8,可选: cv2.LINE_4 cv2.LINE_8 cv2.LINE_AA(抗锯齿,边缘更平滑) |
shift |
坐标小数位数,默认 0,一般不用 |
| 返回值 | 返回绘制后的图像,通常就是传入的 img |
2.3 视频实时检测------Dlib vs OpenCV对比
我们可以同时使用Dlib和OpenCV的Haar分类器进行检测,直观对比两者的效果:
python
import cv2
import dlib
detector = dlib.get_frontal_face_detector()
faceCascade = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')
video = cv2.VideoCapture(0)
while True:
ret, frame = video.read()
frame = cv2.flip(frame, flipCode=1) # 水平翻转(镜像)
image = cv2.resize(frame, (500, 500))
frame = cv2.resize(frame, (500, 500))
if not ret:
break
# Dlib检测
faces1 = detector(frame, 0)
for face in faces1:
x1 = face.left()
y1 = face.top()
x2 = face.right()
y2 = face.bottom()
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
# OpenCV Haar检测
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
faces2 = faceCascade.detectMultiScale(gray, scaleFactor=1.1,
minNeighbors=15, minSize=(5, 5))
for (x, y, w, h) in faces2:
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imshow("Opencv", image)
cv2.imshow('Dlib', frame)
if cv2.waitKey(100) == 27:
break
video.release()
cv2.destroyAllWindows()
对比观察:
-
Dlib检测更稳定,误检少
-
Haar检测速度快,但可能出现误检
-
两者结合使用,可以互补优势
三、人脸关键点定位------68个关键点
3.1 什么是人脸关键点
人脸关键点(Facial Landmarks) 是指人脸上具有特定语义的位置,如眼角、鼻尖、嘴角等。Dlib的68点模型是最经典的关键点检测模型,将人脸划分为68个关键点:
| 编号范围 | 对应部位 | 点数 |
|---|---|---|
| 0-16 | 脸部轮廓(下巴线) | 17 |
| 17-21 | 右眉毛 | 5 |
| 22-26 | 左眉毛 | 5 |
| 27-35 | 鼻子 | 9 |
| 36-41 | 右眼 | 6 |
| 42-47 | 左眼 | 6 |
| 48-59 | 嘴巴外轮廓 | 12 |
| 60-67 | 嘴巴内轮廓 | 8 |
3.2 关键点检测实现
需要先下载预训练模型(可从Dlib官方GitHub获取),本篇使用的模型是:shape_predictor_68_face_landmarks.dat
python
import numpy as np
import cv2
import dlib
# 读取图像
img = cv2.imread("yz1.png")
# 构造人脸检测器
detector = dlib.get_frontal_face_detector()
faces = detector(img, 0)
# 加载关键点预测器
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
for face in faces:
# 获取关键点
shape = predictor(img, face)
# 转换为坐标数组
landmarks = np.array([[p.x, p.y] for p in shape.parts()])
# 绘制每个关键点
for idx, point in enumerate(landmarks):
pos = [point[0], point[1]]
cv2.circle(img, pos, radius=2, color=(0, 255, 0), thickness=-1)
cv2.putText(img, str(idx), pos, cv2.FONT_HERSHEY_SIMPLEX,
0.4, (255, 255, 255), 1, cv2.LINE_AA)
cv2.imshow("img", img)
cv2.waitKey()
cv2.destroyAllWindows()
核心API:
-
dlib.shape_predictor(model_path):加载关键点预测模型 -
predictor(img, face):对单个人脸预测68个关键点 -
shape.parts():返回68个关键点对象,每个点有.x和.y属性
3.3 绘制关键点连线与凸包
有了68个关键点后,我们可以将它们连成线条,或者绘制凸包来表示眼睛、嘴巴等区域:
python
import numpy as np
import dlib
import cv2
def drawLine(start, end):
"""将指定的点连接起来"""
pts = shape[start:end]
for l in range(1, len(pts)):
ptA = tuple(pts[l - 1])
ptB = tuple(pts[l])
cv2.line(image, ptA, ptB, color=(0, 255, 0), thickness=2)
def drawConvexHull(start, end):
"""将指定的点构成凸包"""
Facial = shape[start:end + 1]
mouthHull = cv2.convexHull(Facial)
cv2.drawContours(image, [mouthHull], -1, (0, 255, 0), 2)
image = cv2.imread("yz1.png")
detector = dlib.get_frontal_face_detector()
faces = detector(image, 0)
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
for face in faces:
shape = predictor(image, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
drawConvexHull(36, 41) # 右眼凸包
drawConvexHull(42, 47) # 左眼凸包
drawConvexHull(48, 59) # 嘴巴外轮廓
drawConvexHull(60, 67) # 嘴巴内轮廓
drawLine(0, 17) # 脸颊轮廓
drawLine(17, 22) # 右眉毛
drawLine(22, 27) # 左眉毛
drawLine(27, 36) # 鼻子
cv2.imshow("Frame", image)
cv2.waitKey()
cv2.convexHull():计算点集的凸包(能包含所有点的最小凸多边形),常用于绘制眼睛、嘴巴等区域。
3.4 视频实时关键点检测
将关键点检测应用到摄像头视频流,可以实现实时人脸分析:
python
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
video = cv2.VideoCapture(0)
while True:
ret, frame = video.read()
frame = cv2.flip(frame, 1)
image = frame.copy()
if not ret:
break
faces = detector(frame, 0)
for face in faces:
shape = predictor(frame, face)
landmarks = np.array([[p.x, p.y] for p in shape.parts()])
# 绘制凸包和线条
drawConvexHull(36, 41)
drawConvexHull(42, 47)
drawConvexHull(48, 59)
drawConvexHull(60, 67)
drawLine(0, 17)
drawLine(17, 22)
drawLine(22, 27)
drawLine(27, 36)
# 绘制关键点
for idx, point in enumerate(landmarks):
pos = [point[0], point[1]]
cv2.circle(frame, pos, radius=2, color=(0, 255, 0), thickness=-1)
cv2.putText(frame, str(idx), pos, cv2.FONT_HERSHEY_SIMPLEX,
0.4, (255, 255, 255), 1, cv2.LINE_AA)
cv2.imshow("Frame", frame)
cv2.imshow("image", image)
if cv2.waitKey(100) == 27:
break
video.release()
cv2.destroyAllWindows()
四、表情识别
有了68个关键点,我们就可以计算各种几何特征来判断表情。
4.1 核心指标定义
MAR(Mouth Aspect Ratio,嘴部宽高比) :衡量嘴巴张开的程度。
其中 H 为嘴内部上下的平均距离,D 为嘴的宽度。
代码实现:
python
from sklearn.metrics.pairwise import euclidean_distances
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
MJR(Mouth-to-Jaw Ratio,嘴宽与脸颊宽之比) :衡量嘴巴横向拉伸程度。
其中 D 为嘴的宽度,K 为下颚的宽度或者脸颊的宽度。
python
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
4.2 表情判断逻辑
基于这两个指标,可以判断表情:
python
mar = MAR(shape)
mjr = MJR(shape)
result = "正常" # 默认表情
if mar > 0.5:
result = "大笑"
elif mjr > 0.4:
result = "微笑"
mouthHull = cv2.convexHull(shape[48:61])
frame = cv2add_chinese_text(frame, result, mouthHull[0, 0])
cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
阈值参考:
-
MAR > 0.5 → 嘴巴张开较大 → 大笑
-
MJR > 0.4 → 嘴巴横向拉伸 → 微笑
-
其他 → 正常
4.3 中文输出的实现
OpenCV自带的 cv2.putText() 不支持中文,需要借助PIL库:
python
from PIL import Image, ImageDraw, ImageFont
def cv2add_chinese_text(img, text, position, textcolor=(0, 255, 0), textsize=30):
"""向图片中添加中文"""
if isinstance(img, np.ndarray):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img)
fontstyle = ImageFont.truetype("simsun.ttc", textsize, encoding="utf-8")
draw.text(position, text, textcolor, font=fontstyle)
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
4.4 扩展表情------哭与愤怒
在进阶版本中,我们引入眼睛宽高比(YAR) 来判断更多表情:
python
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))
E = euclidean_distances(shape[37].reshape(1, 2), shape[41].reshape(1, 2))
F = euclidean_distances(shape[38].reshape(1, 2), shape[40].reshape(1, 2))
G = euclidean_distances(shape[36].reshape(1, 2), shape[39].reshape(1, 2))
return ((A + B + C) / 3) / D, ((E + F) / 2) / G
判断逻辑:
python
mar, yar = MAR(shape)
mjr = MJR(shape)
result = "正常"
if mar > 0.5:
result = "大笑"
if yar < 0.25: # 眼睛眯成一条缝
result = "哭"
elif mjr > 0.4:
result = "微笑"
elif yar > 0.3: # 眼睛睁得很大
result = "愤怒"
表情判断矩阵:
| 表情 | MAR | MJR | YAR |
|---|---|---|---|
| 正常 | <0.5 | <0.4 | 0.25-0.3 |
| 微笑 | <0.5 | >0.4 | 正常 |
| 大笑 | >0.5 | - | >0.25 |
| 哭 | >0.5 | - | <0.25 |
| 愤怒 | <0.5 | <0.4 | >0.3 |
五、完整表情识别系统
将以上所有功能整合,构建一个实时的表情识别系统:
python
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))
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))
E = euclidean_distances(shape[37].reshape(1, 2), shape[41].reshape(1, 2))
F = euclidean_distances(shape[38].reshape(1, 2), shape[40].reshape(1, 2))
G = euclidean_distances(shape[36].reshape(1, 2), shape[39].reshape(1, 2))
return ((A + B + C) / 3) / D, ((E + F) / 2) / G
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
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
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, yar = MAR(shape)
mjr = MJR(shape)
result = "正常"
if mar > 0.5:
result = "大笑"
if yar < 0.25:
result = "哭"
elif mjr > 0.4:
result = "微笑"
elif yar > 0.3:
result = "愤怒"
mouthHull = cv2.convexHull(shape[48:61])
eye1Hull = cv2.convexHull(shape[36:42])
eye2Hull = cv2.convexHull(shape[42:48])
frame = cv2add_chinese_text(frame, result, mouthHull[5, 0])
cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
cv2.drawContours(frame, [eye1Hull], -1, (0, 255, 0), 1)
cv2.drawContours(frame, [eye2Hull], -1, (0, 255, 0), 1)
cv2.imshow("Frame", frame)
if cv2.waitKey(1) == 27:
break
cv2.destroyAllWindows()
cap.release()
六、Dlib vs OpenCV 人脸检测对比
| 对比维度 | Dlib HOG | OpenCV Haar |
|---|---|---|
| 检测原理 | HOG特征 + SVM | Haar特征 + 级联分类器 |
| 准确率 | 高 | 中 |
| 误检率 | 低 | 较高 |
| 速度 | 较慢 | 快 |
| 侧脸检测 | 一般 | 差 |
| 小人脸 | 需要上采样 | 需要调参 |
| 关键点 | 支持68点 | 不支持 |
七、总结
本篇博客系统学习了Dlib在人脸分析中的三大应用:
| 技术 | 核心方法 | 关键API |
|---|---|---|
| 人脸检测 | HOG + SVM | get_frontal_face_detector() |
| 关键点定位 | 68点回归模型 | shape_predictor() |
| 表情识别 | 几何特征计算 | MAR、MJR、YAR |