一、项目核心技术栈与实现原理
1.1 核心技术框架
本项目融合了OpenCV、Dlib、NumPy和Scikit-learn四大核心工具:
-
OpenCV:负责摄像头调用、图像预处理、DNN 模型加载、人脸检测、图形绘制(轮廓 / 文字 / 矩形框)
-
Dlib:实现高精度的 68 个人脸特征点检测,为眨眼监测、表情识别提供核心坐标数据
-
NumPy:处理特征点坐标的数组运算,适配各类距离计算和矩阵操作
-
Scikit-learn:提供欧氏距离计算接口,简化特征点之间的距离求解,避免手动编写距离公式。
1.2 核心实现原理
整个系统的核心逻辑围绕人脸检测 - 特征提取 - 指标计算 - 结果判定 - 可视化展示展开:
-
先通过 OpenCV 的 DNN 模块实现快速人脸检测,得到人脸包围框
-
再通过 Dlib 的 68 点特征点预测器,提取人脸关键区域(眼睛、嘴巴、面部轮廓)的坐标
-
计算眼睛纵横比(EAR)、嘴巴纵横比(MAR)、嘴脸比例(MAJ)等核心指标
-
根据指标阈值判定疲劳状态、表情类型,同时通过预训练的 DNN 模型预测年龄和性别
-
最后将所有分析结果实时绘制在画面上,并解决 OpenCV 原生不支持中文显示的问题。
1.3 关键预训练模型与文件
-
OpenCV 人脸检测模型:
opencv_face_detector.pbtxt(配置文件)、opencv_face_detector_uint8.pb(权重文件) -
年龄预测模型:
deploy_age.prototxt(配置文件)、age_net.caffemodel(权重文件) -
性别预测模型:
deploy_gender.prototxt(配置文件)、gender_net.caffemodel(权重文件) -
Dlib68 点特征点预测器:
shape_predictor_68_face_landmarks.dat -
中文显示字体:
simhei.ttf(黑体,解决 OpenCV 中文乱码问题)
二、项目核心模块
2.1中文显示与基础配置
OpenCV 的cv2.putText方法原生不支持中文字符绘制,会出现乱码,因此需要结合 Pillow 库实现中文显示功能,这是所有可视化结果的基础。同时定义项目通用的常量(年龄 / 性别列表、像素均值)。
python
import cv2
import dlib
import numpy as np
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
# 年龄/性别分类列表
ageList = ['0-2岁','4-6岁','8-12岁','15-20岁','25-32岁','38-43岁','48-53岁','60-100岁']
genderList = ['男性','女性']
# 年龄/性别模型输入标准化均值
mean = (78.4263377603,87.7689143744,114.895847746)
# OpenCV添加中文文本(解决原生中文乱码)
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)) # BGR转RGB,适配Pillow
draw = ImageDraw.Draw(img)
# 加载黑体字体,需保证simhei.ttf文件存在
fontStyle = ImageFont.truetype("simhei.ttf", textSize, encoding="utf-8")
draw.text(position, text, textColor, font=fontStyle)
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # RGB转回BGR,适配OpenCV
2.2 人脸检测模块
采用 OpenCV 的 DNN 模块加载预训练的人脸检测模型,实现高效的人脸包围框检测,支持多个人脸同时检测,并绘制人脸矩形框,置信度阈值设为 0.7 以过滤无效检测结果。
cv2.dnn.blobFromImage是 DNN 模型的标准输入处理方法,将原始图像转换为模型可识别的 blob 格式,其中[104,117,123]是人脸检测模型的标准化均值。
python
# 加载预训练模型
faceProto = "model/opencv_face_detector.pbtxt"
faceModel = "model/opencv_face_detector_uint8.pb"
ageProto = "model/deploy_age.prototxt"
ageModel = "model/age_net.caffemodel"
genderProto = "model/deploy_gender.prototxt"
genderModel = "model/gender_net.caffemodel"
ageNet = cv2.dnn.readNet(ageModel,ageProto) # 年龄预测网络
genderNet = cv2.dnn.readNet(genderModel,genderProto) # 性别预测网络
faceNet = cv2.dnn.readNet(faceModel,faceProto) # 人脸检测网络
# 获取人脸包围框并绘制
def getBoxes(net, frame):
frameHeight,frameWidth = frame.shape[:2]
# 构建DNN输入Blob:缩放、归一化、尺寸调整
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])
# 绘制人脸矩形框:绿色、线宽自适应画面高度
cv2.rectangle(frame,(x1,y1),(x2,y2),(0,255,0),int(round(frameHeight /150)),6)
return frame, faceBoxes
2.3 年龄性别检测模块
对检测到的每个人脸区域进行裁剪,转换为年龄 / 性别模型要求的 227×227 尺寸,通过预训练的 Caffe 模型实现快速属性预测,取预测概率最大值作为最终结果,并通过中文显示模块绘制在画面上。
python
frame, faceBoxes = getBoxes(faceNet, frame)
if not faceBoxes:#没有人脸时检测下一顿,后续循环操作不再继续。
print("当前镜头中没有人")
continue
#遍历每一个人脸包国框
for faceBox in faceBoxes:
#处理frame,将其处理为符合DNN输入的格式
x1,y1,x2,y2=faceBox
face=frame[y1:y2,x1:x2]
blob = cv2.dnn.blobFromImage(face,1.0,(227,227),mean)#模型输入为227*277,参考论文
#调用模型,预测性别
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))#输出中文性别和年龄
2.4 疲劳监测模块
通过 Dlib 提取 68 个特征点中左右眼睛的坐标(左眼:42-48,右眼:36-42),计算 EAR 值并绘制眼睛凸包轮廓,实现疲劳监测
EAR眼睛的纵横比是判定疲劳的核心指标,通过计算眼睛上下眼睑的垂直距离与左右眼角的水平距离的比值实现:
-
正常睁眼时,EAR 值稳定在 0.3 以上
-
眨眼时,眼睑闭合,垂直距离骤减,EAR 值会快速低于阈值(本项目设为 0.30)
-
设置计数阈值(50 帧),连续低于阈值时判定为闭眼 / 犯困
函数封装与基础信息:
python
# 计算眼睛纵横比(EAR)
def eye_aspect_ratio(eye):
# 计算眼睛上下眼睑的两个垂直距离
A = euclidean_distances(np.array(eye[1]),np.array(eye[5]))
B = euclidean_distances(np.array(eye[2]),np.array(eye[4]))
# 计算眼睛左右眼角的水平距离
C = euclidean_distances(np.array(eye[0]),np.array(eye[3]))
ear = ((A+ B)/2.0)/C # 计算EAR值
return ear
# 绘制眼睛凸包轮廓
def drawEye(eye):
eyeHuLl = cv2.convexHull(eye) # 生成凸包,拟合眼睛轮廓
cv2.drawContours(frame,[eyeHuLl],-1,(0,255,0),1)
# 初始化Dlib检测器和特征点预测器
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
COUNTER = 0 # 眨眼计数
主程序:
python
faces = detector(frame, 1)
for face in faces:
shape = predictor(frame, face)
shape = np.matrix([[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
if ear < 0.30:
COUNTER += 1
if COUNTER >= 50:
frame = cv2AddChineseText(frame, "!!!别睡了!!!", (250, 250))
else:
COUNTER = 0
drawEye(leftEye)
drawEye(rightEye)
info = "EAR:{:.2f}".format(ear[0][0])
frame = cv2AddChineseText(frame, info, (0, 30))
2.5 表情识别模块
通过 Dlib 提取嘴巴区域的 68 特征点(48-61),计算两个核心指标实现表情分类,分别是嘴巴纵横比(MAR)和嘴脸比例(MAJ),通过阈值划分正常、微笑、大笑三种表情,并绘制嘴巴凸包轮廓。
函数封装:
python
# 计算嘴巴纵横比(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
# 计算嘴脸比例(MAJ):嘴巴左右距离/面部左右宽度
def MAJ(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
主流程:
python
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
mar = MAR(shape)
mjr = MAJ(shape)
result = "正常"
# print('mar', mar, '\tmjr', mjr)
if mar > 0.5:
result = "大笑"
elif mjr > 0.45:
result = "微笑"
mouthHull = cv2.convexHull(shape[48:61])
frame = cv2AddChineseText(frame, result, mouthHull[0, 0])
cv2.drawContours(frame, [mouthHull], -1, (0, 255, 0), 1)
三、结果显示
程序运行后,会弹出实时显示窗口,实现以下功能:
-
绿色矩形框标记检测到的人脸
-
人脸上方显示预测的性别 + 年龄
-
画面左上角显示实时EAR 值,眼睛周围绘制绿色轮廓
-
嘴巴周围绘制绿色轮廓,嘴巴上方显示正常 / 微笑 / 大笑表情
-
当连续闭眼(EAR<0.30)50 帧时,画面中间显示红色警告文字 !!!别睡了!!!