文章目录
- 一、环境准备
- [二、案例一:dlib CNN 人脸检测](#二、案例一:dlib CNN 人脸检测)
-
- [2.1 原理简介](#2.1 原理简介)
- [2.2 完整代码](#2.2 完整代码)
- [2.3 代码解读](#2.3 代码解读)
- [三、案例二:OpenCV DNN 年龄与性别预测](#三、案例二:OpenCV DNN 年龄与性别预测)
-
- [3.1 原理简介](#3.1 原理简介)
- [3.2 完整代码](#3.2 完整代码)
- [3.3 关键 API 说明](#3.3 关键 API 说明)
-
- [3.3.1 `cv2.dnn.readNet(model, config)`](#3.3.1
cv2.dnn.readNet(model, config)) - [3.3.2 `cv2.dnn.blobFromImage(...)`](#3.3.2
cv2.dnn.blobFromImage(...)) - [3.3.3 推理与取结果](#3.3.3 推理与取结果)
- [3.3.4 OpenCV 显示中文](#3.3.4 OpenCV 显示中文)
- [3.3.1 `cv2.dnn.readNet(model, config)`](#3.3.1
- [四、案例三:dlib 68 关键点疲劳检测](#四、案例三:dlib 68 关键点疲劳检测)
-
- [4.1 原理:EAR(Eye Aspect Ratio)](#4.1 原理:EAR(Eye Aspect Ratio))
- [4.2 完整代码](#4.2 完整代码)
- [4.3 代码解读](#4.3 代码解读)
-
- [4.3.1 关键点检测流程](#4.3.1 关键点检测流程)
- [4.3.2 眼睛切片与 EAR](#4.3.2 眼睛切片与 EAR)
- [4.3.3 疲劳判定逻辑](#4.3.3 疲劳判定逻辑)
- [4.3.4 凸包绘制眼睛轮廓](#4.3.4 凸包绘制眼睛轮廓)
- 五、总结
一、环境准备
三个案例均基于 Python 3,主要依赖以下库:
| 库名 | 用途 | 安装命令 |
|---|---|---|
| opencv-python | 图像读取、绘制、摄像头调用、DNN 推理 | pip install opencv-python |
| dlib | 人脸检测、68 关键点定位、CNN 人脸检测 | pip install dlib(Windows 建议先装 CMake/Visual Studio Build Tools) |
| numpy | 数值计算、坐标数组处理 | pip install numpy |
| Pillow | 绘制中文文本(OpenCV 原生不支持中文) | pip install pillow |
| scikit-learn | 提供 euclidean_distances 计算欧氏距离 |
pip install scikit-learn |
说明 :dlib 在 Windows 上直接
pip install可能失败,推荐直接下载对应 Python 版本的预编译 wheel 文件离线安装(可在 GitHub 的 dlib releases 或 pyradiomics 的 wheel 仓库找到对应 cp3x + win_amd64 的预编译包)。
三个案例还需要对应的预训练模型文件,这些文件体积较大,需要单独下载并与脚本放在同一目录:
| 模型文件 | 说明 |
|---|---|
| mmod_human_face_detector.dat | dlib 官方 CNN 人脸检测器(MMOD) |
| shape_predictor_68_face_landmarks.dat | 68 个人脸关键点定位模型 |
| opencv_face_detector_uint8.pb / opencv_face_detector.pbtxt | OpenCV 官方 TensorFlow 人脸检测模型 |
| age_net.caffemodel / deploy_age.prototxt | Caffe 年龄预测模型 |
| gender_net.caffemodel / deploy_gender.prototxt | Caffe 性别预测模型 |
其中年龄、性别预测模型是基于 Caffe 框架训练的经典开源模型,输入尺寸为 227×227;人脸检测模型输入尺寸为 300×300。
二、案例一:dlib CNN 人脸检测
2.1 原理简介
dlib 自带两种人脸检测器:
- HOG + SVM 检测器 (
dlib.get_frontal_face_detector()):速度快,但在侧脸、遮挡、小目标场景下准确率有限。 - MMOD CNN 检测器 (
dlib.cnn_face_detection_model_v1()):全称为 Max-Margin Object Detection,是一个卷积神经网络检测器,准确率明显高于 HOG,代价是推理速度更慢。
本案例使用的就是第二种 CNN 检测器。
2.2 完整代码
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("people.jpg")
# 检测人脸,第二个参数 2 表示将图像上采样 2 倍后再检测,
# 这样可以检测到更小的人脸;上采样倍数越大,检测到的小脸越多,但速度越慢。
faces = cnn_face_detector(img, 2)
for d in faces:
# 计算每个人脸的位置
rect = d.rect
left = rect.left()
top = rect.top()
right = rect.right()
bottom = rect.bottom()
# 绘制人脸对应的矩形框
cv2.rectangle(img, pt1=(left, top), pt2=(right, bottom),
color=(0, 255, 0), thickness=3)
cv2.imshow("result", img)
cv2.waitKey()
cv2.destroyAllWindows()
2.3 代码解读
dlib.cnn_face_detection_model_v1("mmod_human_face_detector.dat"):加载 CNN 人脸检测模型。cnn_face_detector(img, 2):对图像做检测,第二个参数2表示将图像放大 2 倍后再送入网络,目的是提升对小目标人脸的检出率。- 返回的
faces是一个mmod_rectangles列表,每个元素d都包含一个rect成员,对应人脸的矩形框。 rect.left() / top() / right() / bottom():获取矩形框的四个边界坐标。- 最后用
cv2.rectangle在原图上画出绿色检测框。
三、案例二:OpenCV DNN 年龄与性别预测
3.1 原理简介
OpenCV 的 cv2.dnn 模块支持直接加载多种框架训练好的模型(Caffe、TensorFlow、Darknet 等),无需安装深度学习框架即可完成推理。本项目串联了三个模型:
摄像头帧 → 人脸检测模型(找出人脸框)→ 裁剪人脸 → 性别模型 → 性别
→ 裁剪人脸 → 年龄模型 → 年龄段
- 人脸检测模型:输入整帧图像,输出若干候选框及置信度。
- 年龄模型 :把人脸区域缩放到
227×227,输出 8 个年龄段的概率。 - 性别模型 :同样输入
227×227人脸,输出"男性/女性"两类概率。
年龄模型输出与下列 ageList 一一对应:
python
ageList = ['0-2岁', '4-6岁', '8-12岁', '15-20岁',
'25-32岁', '38-43岁', '48-53岁', '60-100岁']
性别模型输出与下列列表一一对应:
python
genderList = ['男性', '女性']
3.2 完整代码
python
import cv2
from PIL import Image, ImageDraw, ImageFont
import numpy as np
# =====模型初始化=======
# 人脸检测模型配置与权重
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"
# 加载DNN网络
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-20岁',
'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,准备送入网络
blob = cv2.dnn.blobFromImage(frame, scalefactor=1.0, size=(300, 300),
mean=[104, 117, 123], swapRB=True, crop=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
# OpenCV不支持中文,借助PIL绘制中文
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
if isinstance(img, np.ndarray):
# BGR转RGB
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)
# RGB转回BGR
return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
cap = cv2.VideoCapture(0) # 打开摄像头
while True:
_, frame = cap.read() # 读取一帧
frame = cv2.flip(frame, flipCode=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, scalefactor=1.0, size=(227, 227), mean=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: # ESC退出
break
cv2.destroyAllWindows()
cap.release()
3.3 关键 API 说明
3.3.1 cv2.dnn.readNet(model, config)
加载深度学习模型。第一个参数是权重文件,第二个参数是网络结构配置文件。不同框架的后缀不同:
- Caffe:
.caffemodel+.prototxt - TensorFlow:
.pb/.pb2+.pbtxt
3.3.2 cv2.dnn.blobFromImage(...)
把普通图像转换为网络要求的 blob 格式,相当于做了一次前处理:
scalefactor:像素值缩放比例,本项目用1.0;size:网络要求的输入尺寸,人脸检测用300×300,年龄/性别用227×227;mean:训练时使用的均值,推理时必须减去,否则结果会明显偏差;swapRB=True:OpenCV 读入的是 BGR 顺序,而很多模型训练时用的是 RGB,需要交换红蓝通道;crop=False:不裁剪,直接 resize。
3.3.3 推理与取结果
python
net.setInput(blob)
outs = net.forward()
net.forward() 返回网络的输出。人脸检测输出是四维数组 (1, 1, N, 7),其中:
N:候选框数量;- 每一行的第 3 个值(索引
2)是置信度; - 第 4~7 个值(索引
3~6)是归一化后的x1, y1, x2, y2,需要乘以图像宽高还原成像素坐标。
对于年龄/性别分类模型,outs[0] 就是各类别概率,用 argmax() 取最大概率对应的索引即可。
3.3.4 OpenCV 显示中文
OpenCV 的 cv2.putText() 不支持中文字体,直接写中文会显示成乱码方块。通用做法是:
- 把 OpenCV 的 BGR 图像转成 PIL 的 RGB 图像;
- 用 PIL 的
ImageDraw.text()绘制中文(指定 TTF/TTC 字体); - 再把 PIL 图像转回 OpenCV 的 BGR 格式。
cv2AddChineseText() 函数封装的就是这一套流程。Windows 上可以直接使用系统自带的 simsun.ttc(宋体)。
四、案例三:dlib 68 关键点疲劳检测
4.1 原理:EAR(Eye Aspect Ratio)
疲劳检测的核心思路是:当人疲劳犯困时,眨眼频率会下降、闭眼持续时间会变长 。通过 dlib 的 68 点关键点模型定位眼睛位置,再用一个简单的几何指标------眼睛纵横比 EAR------来量化眼睛的"睁开程度"。
对于一只眼睛的 6 个关键点(按顺序记为 p 1 , p 2 , p 3 , p 4 , p 5 , p 6 p_1, p_2, p_3, p_4, p_5, p_6 p1,p2,p3,p4,p5,p6):
- p 1 , p 4 p_1, p_4 p1,p4 分别是眼睛的内、外眼角(水平方向);
- p 2 , p 3 p_2, p_3 p2,p3 是上眼睑的两个点;
- p 5 , p 6 p_5, p_6 p5,p6 是下眼睑的两个点。
EAR 的计算公式为:
EAR = ∥ p 2 − p 6 ∥ + ∥ p 3 − p 5 ∥ 2 ⋅ ∥ p 1 − p 4 ∥ \text{EAR} = \frac{\|p_2 - p_6\| + \|p_3 - p_5\|}{2 \cdot \|p_1 - p_4\|} EAR=2⋅∥p1−p4∥∥p2−p6∥+∥p3−p5∥
分子是上下眼睑之间两段垂直距离的平均值,分母是眼睛的水平宽度。当眼睛睁开时,EAR 基本稳定在一个较大的值;当眼睛闭上时,分子趋近于 0,EAR 也趋近于 0。 这个阈值不需要逐帧训练,通常在 0.2~0.3 之间。
在 68 点模型中,关键点索引与眼睛的对应关系为:
- 右眼:
36 ~ 41(在数组中切片为shape[36:42]) - 左眼:
42 ~ 47(在数组中切片为shape[42:48])

每只眼睛切片后得到的 6 个点,按顺序正好对应公式中的 p 1 ∼ p 6 p_1 \sim p_6 p1∼p6。
4.2 完整代码
python
import numpy as np
import dlib
import cv2
from sklearn.metrics.pairwise import euclidean_distances
from PIL import Image, ImageDraw, ImageFont
# 计算眼睛纵横比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公式
ear = ((A + B) / 2.0) / C
return ear
# 在图片上绘制中文,opencv本身不支持中文,借助PIL实现
def cv2AddChineseText(img, text, position, textColor=(0, 255, 0), textSize=30):
"""向图片中添加中文
OpenCV 原生不支持中文,借助 PIL 库绘制中文,再转回 OpenCV 图像格式
:param img: opencv 图像
:param text: 需要显示的中文文本
:param position: 文字绘制坐标(x,y)
:param textColor: 文字 BGR 颜色
:param textSize: 文字字号
:return: 添加完中文后的 opencv 图像
"""
if isinstance(img, np.ndarray): # 判断是否 OpenCV 图片类型
# OpenCV BGR 格式转为 PIL 的 RGB 格式
img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img) # 在 img 图片上创建一个绘图对象
# 字体的格式,simsun.ttc 为 Windows 自带宋体
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) # PIL RGB 转回 OpenCV BGR 格式
# 绘制眼睛轮廓
def drawEye(eye):
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
# 闭眼帧计数器
CONTER = 0
# 初始化dlib人脸检测器和68点关键点预测器
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:
# 获取人脸68个关键点
shape = predictor(frame, face)
# 将关键点转为numpy数组 (68,2)
shape = np.array([[p.x, p.y] for p in shape.parts()])
# 提取右眼、左眼关键点
rightEye = shape[36:42]
leftEye = shape[42:48]
# 计算左右眼EAR值
rightEAR = eye_aspect_ratio(rightEye)
leftEAR = eye_aspect_ratio(leftEye)
# 取双眼EAR平均值
ear = (leftEAR + rightEAR) / 2.0
# EAR小于阈值判定闭眼,连续累计帧数
if ear < 0.3:
CONTER += 1
# 连续50帧闭眼,判定疲劳,输出警告
if CONTER >= 50:
frame = cv2AddChineseText(frame, '!!!危险!!!', (250, 250))
else:
# 眼睛睁开,计数器清零
CONTER = 0
# 绘制双眼轮廓
drawEye(leftEye)
drawEye(rightEye)
# 显示EAR数值
info = 'EAR: {:.2f}'.format(ear[0][0])
frame = cv2AddChineseText(frame, info, (0, 30))
cv2.imshow('Frame', frame)
# ESC退出
if cv2.waitKey(1) == 27:
break
# 释放资源
cap.release()
cv2.destroyAllWindows()
4.3 代码解读
4.3.1 关键点检测流程
python
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor('shape_predictor_68_face_landmarks.dat')
faces = detector(frame, 0)
for face in faces:
shape = predictor(frame, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
detector(frame, 0):先用 HOG 检测器找到人脸框,第二个参数0表示不上采样。predictor(frame, face):在每个人脸框内预测 68 个关键点。shape.parts()返回 68 个(x, y)点,列表推导式把它转成(68, 2)的 numpy 数组,方便切片。
4.3.2 眼睛切片与 EAR
python
rightEye = shape[36:42] # 右眼 6 个点
leftEye = shape[42:48] # 左眼 6 个点
切片后传入 eye_aspect_ratio(),按公式计算每只眼睛的 EAR,再取左右眼平均值作为最终 EAR:
python
ear = (leftEAR + rightEAR) / 2.0
4.3.3 疲劳判定逻辑
python
if ear < 0.3:
CONTER += 1
if CONTER >= 50:
frame = cv2AddChineseText(frame, '!!!危险!!!', (250, 250))
else:
CONTER = 0
这里有两个关键超参数:
- 阈值
0.3:EAR 低于此值视为"闭眼"。不同人、不同摄像头角度下最优阈值会有差异,可按实际效果微调。 - 连续帧数
50:单帧闭眼可能只是正常眨眼,所以需要连续 50 帧(按 30 FPS 计算约 1.6 秒)都低于阈值才报警,避免误报。
4.3.4 凸包绘制眼睛轮廓
python
def drawEye(eye):
eyeHull = cv2.convexHull(eye)
cv2.drawContours(frame, [eyeHull], -1, (0, 255, 0), 1)
cv2.convexHull() 计算一组点的凸包,再用 cv2.drawContours() 画出眼睛的外轮廓,便于直观调试。
五、总结
本文串联了计算机视觉入门阶段三个典型的案例:
| 案例 | 核心技术 | 学习要点 |
|---|---|---|
| CNN 人脸检测 | dlib MMOD、矩形框绘制 | 加载预训练模型、坐标获取 |
| 年龄性别预测 | OpenCV DNN、Caffe/TensorFlow 模型 | blobFromImage、多模型串联、中文绘制 |
| 疲劳检测 | dlib 68 关键点、EAR 几何公式 | 几何特征设计、时序计数报警 |