在计算机视觉领域,人脸检测及相关特征分析是最基础也最具应用价值的方向之一。无论是驾驶员疲劳监控、课堂学员状态检测,还是日常的年龄性别识别、表情分析,都能通过OpenCV、Dlib等工具快速实现。本文将结合4个实战项目,从基础到综合,详细讲解人脸检测、疲劳检测、年龄性别预测、表情识别的核心实现逻辑,附上完整可运行代码,适合入门者快速上手实践。
一、前言:
核心工具与应用场景 本次实战主要依赖两大核心工具:OpenCV(用于图像读取、预处理、模型加载和画面展示)和Dlib(用于人脸检测、68个人脸关键点定位,精度高、易用性强),辅助使用sklearn计算欧氏距离、PIL解决OpenCV中文显示问题。 适用场景涵盖:
• 驾驶员疲劳监控:实时检测闭眼状态,避免危险驾驶
• 课堂/办公状态检测:识别学员/员工疲劳、专注度
• 人脸基础特征分析:快速识别年龄、性别、表情,适用于零售、安防等场景
• 基础人脸检测:基于CNN的高精度人脸定位,为后续分析提供基础
注:所有代码均已调试可运行,需提前准备对应模型文件(如shape_predictor_68_face_landmarks.dat、各类CNN模型文件),具体安装依赖和模型获取方式会在对应模块说明。
二、实战项目一:基础疲劳检测(驾驶员/学员监控核心)
2.1 核心功能 通过Dlib检测人脸及68个关键点,计算眼睛纵横比(EAR)判断眼睛闭合状态,当连续闭眼超过设定帧数时,触发疲劳预警,可直接用于驾驶员、学员等场景的状态监控。
2.2 关键技术解析 核心逻辑:眼睛纵横比(EAR)是判断眼睛闭合的核心指标------眼睛睁开时,EAR值较高;眼睛闭合时,EAR值会显著降低,设定阈值(默认0.3)即可区分睁眼/闭眼,再通过计数器统计连续闭眼帧数,触发预警。
关键函数说明:
• eye_aspect_ratio(eye):计算单只眼睛的EAR值,通过欧氏距离计算眼睛垂直、水平方向距离,最终得到纵横比。
• drawEye(frame, eye):绘制眼睛凸包轮廓,方便可视化观察眼睛状态。
• cv2AddChineseText(img, text, position):解决OpenCV不支持中文显示的问题,通过PIL转换图像格式后绘制中文。
python
"""
疲劳检测程序,可用于驾驶员监控、学员上课状态检测等
功能说明:
1. 使用dlib检测人脸和68个关键点
2. 计算眼睛纵横比(EAR)判断眼睛闭合状态
3. 当连续闭眼超过设定帧数时触发疲劳预警
"""
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances # 计算欧氏距离
# pip install scikit-learn -i https://pypi.tuna.tsinghua.edu.cn/simple
from PIL import Image, ImageDraw, ImageFont # 用于在OpenCV图像上绘制中文
# pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
def eye_aspect_ratio(eye):
"""
计算眼睛纵横比(Eye Aspect Ratio, EAR)
:param eye: 眼睛的6个关键点坐标(dlib中,每只眼睛对应6个点)
:return: EAR值,值越小表示眼睛越接近闭合
"""
# 计算眼睛垂直方向的两组距离(上眼睑到下眼睑)
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值(纵横比)
ear = (A + B) / (2.0 * C)
return ear[0][0]
def cv2AddChineseText(img, text, position, textColor=(255, 0, 0), textSize=50):
"""
在OpenCV图像上添加中文文本(解决cv2不支持中文的问题)
:param img: 原始图像
:param text: 要添加的中文文本
:param position: 文本位置(x, y)
:param textColor: 文本颜色,默认红色
:param textSize: 文本大小,默认50
:return: 添加文本后的图像
"""
# 将OpenCV的BGR格式转换为PIL的RGB格式
if isinstance(img, np.ndarray):
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
# 创建绘图对象
draw = ImageDraw.Draw(img)
# 加载中文字体(Windows系统默认黑体,其他系统可替换字体路径)
try:
font = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
except IOError:
# 如果找不到黑体,使用默认字体(可能不支持中文)
font = ImageFont.load_default()
# 在图像上绘制文本
draw.text(position, text, textColor, font=font)
# 将图像转换回OpenCV的BGR格式
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
def drawEye(frame, eye):
"""
绘制眼睛的凸包轮廓,方便可视化
:param frame: 原始图像
:param eye: 眼睛的关键点坐标
"""
# 计算眼睛的凸包(最小包围轮廓)
eyeHull = cv2.convexHull(eye)
# 在图像上绘制凸包轮廓(绿色线条)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
# 全局变量初始化
COUNTER = 0 # 闭眼持续次数统计
# 初始化dlib人脸检测器和关键点预测器
detector = dlib.get_frontal_face_detector() # 构造脸部位置检测器
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 读取人脸关键点定位模型
# 打开摄像头(Windows系统添加CAP_DSHOW避免权限问题)
cap = cv2.VideoCapture(0)
while True:
# 读取摄像头帧
ret, frame = cap.read()
faces = detector(frame,0)
# 遍历检测到的每一张人脸
for face in faces:
# 获取人脸的68个关键点
shape = predictor(frame, face) # 获取关键点
# 将关键点转换为(x, y)坐标形式的numpy数组
shape = np.array([[p.x, p.y] for p in shape.parts()])
# 提取左右眼的关键点(dlib标准索引:右眼36-41,左眼42-47)
rightEye = shape[36:42] # 右眼,关键点索引从36到41(不包含42)
leftEye = shape[42:48] # 左眼,关键点索引从42到47(不包含48)
# 计算左右眼的EAR值
rightEAR = eye_aspect_ratio(rightEye) # 计算右眼纵横比
leftEAR = eye_aspect_ratio(leftEye) # 计算左眼纵横比
# 取左右眼的平均值作为最终EAR值
ear = (leftEAR + rightEAR) / 2.0
# 判断眼睛状态
if ear < 0.3: # 小于0.3判定为闭眼,也可能是眨眼
COUNTER += 1 # 每检测到一次闭眼,计数器+1
# 连续闭眼超过设定帧数,触发疲劳预警
if COUNTER >= 50:
# 在图像上显示预警文字
frame = cv2AddChineseText(frame, "!!!! 危险 !!!!", (250, 250))
else: # 宽高比>0.3,判定为睁眼,重置计数器
COUNTER = 0 # 闭眼次数清零
# 绘制左右眼的凸包轮廓(仅睁眼时绘制,避免闭眼时轮廓不清晰)
drawEye(frame,leftEye) # 绘制左眼凸包
drawEye(frame,rightEye) # 绘制右眼凸包
# 在图像上显示当前EAR值
info = "EAR: {:.2f}".format(ear)
frame = cv2AddChineseText(frame, info, position=(0, 30))
# 显示处理后的图像
cv2.imshow("Frame", frame)
# 按下ESC键退出程序
if cv2.waitKey(1) == 27:
break
# 释放摄像头资源和窗口
cv2.destroyAllWindows()
cap.release()
#
三、实战项目二:年龄性别预测(快速人脸特征分析)
3.1 核心功能 基于OpenCV的DNN模块加载预训练模型,实现人脸检测、年龄预测(8个年龄段)、性别预测(男/女),可快速获取人脸基础特征,适用于零售、安防等场景。
核心逻辑:使用预训练的人脸检测模型(opencv_face_detector)获取人脸包围框,再通过年龄模型(age_net)、性别模型(gender_net)对人脸区域进行推理,输出预测结果。
关键注意点:
• 模型文件需与代码放在同一目录,包括faceProto、faceModel、ageProto、ageModel、genderProto、genderModel。
• 通过blobFromImage对人脸图像进行预处理,符合模型输入要求。
• 置信度阈值设为0.7,过滤低置信度人脸,提升预测精度。
python
import cv2
from PIL import Image,ImageDraw,ImageFont
import numpy as np
#模型初始化
faceProto = "opencv_face_detector.pbtxt"
faceModel = "opencv_face_detector_uint8.pb"
ageProto = "deploy_age.prototxt"
ageModel = "age_net.caffemodel"
genderProto = "deploy_gender.prototxt"
genderModel = "gender_net.caffemodel"
#加载网络
ageNet = cv2.dnn.readNet(ageModel,ageProto)
genderNet = cv2.dnn.readNet(genderModel,genderProto)
faceNet = cv2.dnn.readNet(faceModel,faceProto)
#变量初始化
ageList=['0-2岁','4-6岁','8-12岁','15-25岁','25-32岁','38-43岁','48-53岁','60-100岁']
genderList = ['男性','女性']
mean =(78.4263377603,87.7689143744,114.895847746)
#自定义函数,获取人脸包围框
def getBoxes(net,frame):
frameHeight,frameWidth = frame.shape[:2]
blob = cv2.dnn.blobFromImage(frame,1.0,(300,300),
[104,117,123],True,False)
net.setInput(blob)
detections = net.forward()
faceBoxes=[]
xx=detections.shape[2]
for i in range(detections.shape[2]):
confidence = detections[0,0,i,2]
if confidence >0.7:
x1 = int(detections[0,0,i,3]*frameWidth)
y1 = int(detections[0,0,i,4]*frameHeight)
x2 = int(detections[0,0,i,6]*frameWidth)
y2 = int(detections[0,0,i,6]*frameHeight)
faceBoxes.append([x1,y1,x2,y2])
cv2.rectangle(frame,(x1,y1),(x2,y2),
(0,255,0),int(round(frameHeight/150)),6)
return frame,faceBoxes
def cv2AddChineseText(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)
# 加载中文字体(Windows系统默认黑体,其他系统可替换字体路径)
try:
font = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
except IOError:
# 如果找不到黑体,使用默认字体(可能不支持中文)
font = ImageFont.load_default()
# 在图像上绘制文本
draw.text(position, text, textColor, font=font)
# 将图像转换回OpenCV的BGR格式
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
cap = cv2.VideoCapture(0)
while True:
_,frame = cap.read()
frame = cv2.flip(frame,1)
frame,faceBoxes = getBoxes(faceNet,frame)
if not faceBoxes:
print("当前镜头中没有人")
continue
for faceBox in faceBoxes:
x1,y1,x2,y2=faceBox
face = frame[y1:y2,x1:x2]
blob = cv2.dnn.blobFromImage(face,1.0,(227,227),mean)
#调用模型,预测性别
genderNet.setInput(blob)
genderOuts = genderNet.forward()
gender = genderList[genderOuts[0].argmax()]
#调用模型,预测年龄
ageNet.setInput(blob)
ageOuts = ageNet.forward()
age = ageList[ageOuts[0].argmax()]
result = "{},{}".format(gender,age)
frame = cv2AddChineseText(frame,result,(x1,y1-30))
cv2.imshow("result",frame)
if cv2.waitKey(1) ==27:
break
cv2.destroyAllWindows()
cap.release()
四、实战项目三:三合一检测(疲劳+年龄性别+表情)
4.1 核心功能 整合前两个项目的核心功能,同时实现疲劳检测、年龄性别预测、表情识别(正常/微笑/大笑),是更贴近实际应用的综合项目,可直接用于多场景人脸状态监控。
4.2 关键技术解析 在疲劳检测、年龄性别预测的基础上,新增表情识别功能,核心依赖两个关键指标:
• MAR(嘴巴纵横比):通过计算嘴巴关键点的欧氏距离,判断嘴巴张开程度,MAR>0.5判定为大笑。
• MJR(嘴巴下颌比):通过嘴巴宽度与下颌宽度的比值,判断微笑状态,MJR>0.45判定为微笑。
核心优化:解决了摄像头占用、黑屏、无效帧等问题,增加摄像头重试机制、帧有效性校验,确保程序稳定运行;同时优化中文显示、画面布局,提升可视化效果。
python
import numpy as np
import dlib
import cv2
import time
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
# ====================== 1. 全局配置 & 模型加载 ======================
# 疲劳检测阈值
EAR_THRESHOLD = 0.3 # 眼睛纵横比阈值(小于此值为闭眼)
EYE_AR_CONSEC_FRAMES = 12 # 连续闭眼帧数触发预警
COUNTER = 0 # 闭眼计数器
# 年龄性别模型配置
ageList = ['0-2岁', '4-6岁', '8-12岁', '15-25岁', '25-32岁', '38-43岁', '48-53岁', '60-100岁']
genderList = ['男性', '女性']
MODEL_MEAN_VALUES = (78.4263377603, 87.7689143744, 114.895847746)
# 加载所有模型(必须和代码放在同一文件夹!)
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
ageNet = cv2.dnn.readNet("age_net.caffemodel", "deploy_age.prototxt")
genderNet = cv2.dnn.readNet("gender_net.caffemodel", "deploy_gender.prototxt")
faceNet = cv2.dnn.readNet("opencv_face_detector_uint8.pb", "opencv_face_detector.pbtxt")
# ====================== 2. 核心功能函数 ======================
def eye_aspect_ratio(eye):
"""计算眼睛纵横比EAR(疲劳检测)"""
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[0][0]
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[0][0]
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[0][0] / J[0][0]
def drawEye(frame, eye):
"""绘制眼睛轮廓"""
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
def getBoxes(net, frame):
"""获取人脸框(年龄性别检测)"""
frameHeight, frameWidth = frame.shape[:2]
blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), [104, 117, 123], True, False)
net.setInput(blob)
detections = net.forward()
faceBoxes = []
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > 0.7:
x1 = int(detections[0, 0, i, 3] * frameWidth)
y1 = int(detections[0, 0, i, 4] * frameHeight)
x2 = int(detections[0, 0, i, 5] * frameWidth)
y2 = int(detections[0, 0, i, 6] * frameHeight)
faceBoxes.append([x1, y1, x2, y2])
return frame, faceBoxes
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
"""OpenCV显示中文(兼容所有系统)"""
if img is None or img.size == 0:
return img
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
# 字体兜底,找不到字体不报错
try:
font = ImageFont.truetype("simhei.ttf", textSize)
except:
font = ImageFont.load_default()
draw.text(position, text, textColor, font=font)
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# ====================== 3. 主程序(三合一功能) ======================
def run():
global COUNTER
# 摄像头初始化(防占用+防黑屏核心修复)
cap = None
for retry in range(5):
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
if cap.isOpened():
print("✅ 摄像头打开成功!")
break
print(f" 第{retry + 1}次重试,2秒后再试...")
time.sleep(2)
if not cap.isOpened():
print("摄像头打开失败!")
return
while True:
ret, frame = cap.read()
# 核心:跳过无效帧,杜绝黑屏/乱码
if not ret or frame is None or frame.size == 0:
continue
frame = np.ascontiguousarray(frame)
# 1. 年龄+性别检测
frame, faceBoxes = getBoxes(faceNet, frame)
# 2. dlib人脸+关键点检测(疲劳+表情)
faces = detector(frame, 0)
# 遍历所有人脸
for face in faces:
# 68关键点检测
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
# ============= 疲劳检测 =============
leftEye = shape[42:48]
rightEye = shape[36:42]
leftEAR = eye_aspect_ratio(leftEye)
rightEAR = eye_aspect_ratio(rightEye)
ear = (leftEAR + rightEAR) / 2.0
# 绘制眼睛轮廓
drawEye(frame, leftEye)
drawEye(frame, rightEye)
# 疲劳判断
if ear < EAR_THRESHOLD:
COUNTER += 1
if COUNTER >= EYE_AR_CONSEC_FRAMES:
frame = cv2AddChineseText(frame, " 疲劳预警!", (220, 200), (0, 0, 255), 40)
else:
COUNTER = 0
# 显示EAR值
frame = cv2AddChineseText(frame, f"EAR: {ear:.2f}", (10, 30), (255, 0, 0), 30)
# ============= 表情识别 =============
mar = MAR(shape)
mjr = MJR(shape)
result = "正常"
if mar > 0.5:
result = "大笑"
elif mjr > 0.45:
result = "微笑"
# 绘制嘴巴轮廓
mouthHull = cv2.convexHull(shape[48:61])
cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
frame = cv2AddChineseText(frame, result, (face.left(), face.top() - 20), (0, 255, 0), 30)
# ============= 年龄性别结果绘制 =============
for faceBox in faceBoxes:
x1, y1, x2, y2 = faceBox
face_img = frame[y1:y2, x1:x2]
if face_img.size == 0:
continue
blob = cv2.dnn.blobFromImage(face_img, 1.0, (227, 227), MODEL_MEAN_VALUES, swapRB=False)
# 预测性别
genderNet.setInput(blob)
gender = genderList[genderNet.forward()[0].argmax()]
# 预测年龄
ageNet.setInput(blob)
age = ageList[ageNet.forward()[0].argmax()]
# 显示年龄性别
frame = cv2AddChineseText(frame, f"{gender},{age}", (x1, y1 - 30), (255, 255, 0), 25)
# 显示最终画面
cv2.imshow("三合一检测:疲劳+年龄性别+表情", frame)
# ESC退出
if cv2.waitKey(1) & 0xFF == 27:
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
run()
五、实战项目四:CNN人脸检测
5.1 核心功能
基于Dlib的CNN人脸检测模型,实现高精度人脸定位,绘制人脸包围框,为后续的疲劳检测、表情识别等功能提供基础,相比传统Haar级联检测器,精度更高、抗干扰能力更强。
5.2 关键技术解析
核心逻辑:使用Dlib的cnn_face_detection_model_v1加载预训练CNN模型(mmod_human_face_detector.dat),检测图像中的人脸,获取人脸矩形框坐标,绘制包围框并显示结果。
python
import dlib
import cv2
# opencv可以直接通过readnet来读取神经网络。 dlib也可以的。
cnn_face_detector = dlib.cnn_face_detection_model_v1("mmod_human_face_detector.dat")
img = cv2.imread("hg1.png")
faces = cnn_face_detector(img, 0) # 检测人脸
for d in faces:
# 计算每个人脸的位置
rect = d.rect
left = rect.left()
top = rect.top()
right = rect.right()
bottom = rect.bottom()
# 绘制人脸对应的矩形框
cv2.rectangle(img, (left, top), (right, bottom), (0, 255, 0), 3)
cv2.imshow("result", img)
k = cv2.waitKey()
cv2.destroyAllWindows()
需准备以下模型文件,放在代码同一目录:
• shape_predictor_68_face_landmarks.dat:Dlib 68关键点检测模型
• mmod_human_face_detector.dat:Dlib CNN人脸检测模型
• opencv_face_detector.pbtxt、opencv_face_detector_uint8.pb:OpenCV人脸检测模型 • deploy_age.prototxt、age_net.caffemodel:年龄预测模型
• deploy_gender.prototxt、gender_net.caffemodel:性别预测模型 模型可通过Dlib官方仓库、OpenCV官方模型库或网络公开资源获取,确保模型版本与代码兼容。
六、常见问题与解决方案
• 问题1:摄像头黑屏、无法打开? 解决方案:关闭微信、钉钉等占用摄像头的软件;Windows系统添加cv2.CAP_DSHOW后端;检查摄像头权限和驱动。
• 问题2:中文显示乱码? 解决方案:使用cv2AddChineseText函数,确保系统有simhei.ttf字体,若没有可替换为其他支持中文的字体路径。
• 问题3:疲劳检测不触发预警? 解决方案:调整EAR阈值(根据自身眼睛大小微调,建议0.28-0.32);降低连续闭眼帧数(建议10-15帧,对应0.3-0.5秒)。
• 问题4:模型加载失败? 解决方案:检查模型文件路径是否正确,确保模型文件完整,未损坏。
七、总结与延伸
本文通过4个实战项目,从基础的CNN人脸检测,到单一功能的疲劳检测、年龄性别预测,再到综合的三合一检测,完整覆盖了人脸相关检测的核心应用。这些项目的核心逻辑简单易懂,代码可直接复用,适合入门者快速掌握OpenCV、Dlib的使用,也可根据实际需求进行优化:
• 优化检测速度:对图像进行缩放、裁剪,减少计算量,适配实时监控场景。
• 提升精度:调整阈值、更换更优的预训练模型,或添加数据增强,适配复杂场景。
• 扩展功能:添加人脸解锁、专注度分析、异常状态报警(如大声喧哗)等功能。
人脸检测及相关特征分析是计算机视觉的入门基础,掌握这些技能后,可进一步探索更复杂的应用,如人脸识别、人脸活体检测、情感分析等。希望本文能为大家提供实用的参考,助力大家快速上手实战!