【计算机视觉入门】基于 dlib 与 OpenCV 的三大案例:人脸检测、年龄性别预测与疲劳检测

文章目录

  • 一、环境准备
  • [二、案例一: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 显示中文)
  • [四、案例三: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 自带两种人脸检测器:

  1. HOG + SVM 检测器dlib.get_frontal_face_detector()):速度快,但在侧脸、遮挡、小目标场景下准确率有限。
  2. 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() 不支持中文字体,直接写中文会显示成乱码方块。通用做法是:

  1. 把 OpenCV 的 BGR 图像转成 PIL 的 RGB 图像;
  2. 用 PIL 的 ImageDraw.text() 绘制中文(指定 TTF/TTC 字体);
  3. 再把 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 几何公式 几何特征设计、时序计数报警
相关推荐
yyk333241 小时前
OpenCV中的表情识别
opencv
爱吃火鸡面呀1 小时前
基于 dlib 与 OpenCV 的人脸换脸实战:从关键点检测到无缝融合
opencv·目标检测
爱吃火鸡面呀1 小时前
MediaPipe 人体姿态估计与面部网格实战:从关键点检测到实时绘制
opencv
zx_741484811 小时前
【计算机视觉入门】OpenCV + MediaPipe + dlib:从仿射变换到换脸、手势、姿态与人脸网格
python·opencv·计算机视觉
棣廷1 小时前
初识OpenCV——疲劳检测
人工智能·opencv·目标检测
sali-tec4 小时前
C# 基于OpenCv的视觉工作流-章108-区域筛选
图像处理·人工智能·opencv·算法·计算机视觉
YOLO数据集集合13 小时前
高铁轨道紧固件损坏检测数据集 | 高铁巡检 紧固件缺陷 轨道安全9093期
人工智能·目标检测·计算机视觉·目标跟踪·轨道·铁轨紧固件·铁轨
潜心一志19 小时前
HALCON软件——基础语法
学习·计算机视觉
毋小黑21 小时前
753K 参数打赢 SwinIR:CRAFT 用「高频先验」给 Transformer 超分查漏补缺
人工智能·opencv·计算机视觉