1. 项目概述

在计算机视觉领域,人脸相关技术一直是最具实用价值的研究方向之一。作为一名长期从事视觉算法开发的工程师,我经常需要为不同行业客户定制人脸分析解决方案。今天要分享的是两个最常被需求的实战项目:基于眼睛纵横比(EAR)的实时人脸疲劳检测,以及基于预训练CNN模型的年龄性别识别系统。

这两个技术在实际应用中有着广泛场景:

  • 疲劳检测:可应用于驾驶员状态监控、工业安全生产、远程考试监考等场景
  • 年龄性别识别:可用于智能零售、广告投放、安防监控等业务

本文将使用Python+OpenCV+dlib技术栈,从环境搭建到完整实现,手把手带你完成这两个经典项目。不同于官方文档的抽象说明,我会重点分享在实际部署中的工程细节和调优经验。

2. 环境准备与工具选型

2.1 开发环境配置

推荐使用Python 3.8+环境,这是目前最稳定的版本组合。以下是必须安装的核心库:

pip install opencv-python==4.5.5.64
pip install dlib==19.24.0
pip install scikit-learn==1.0.2
pip install pillow==9.0.1

注意:dlib安装可能需要先安装CMake和Visual Studio Build Tools(Windows)或g++(Linux)。如果遇到编译问题,可以直接下载预编译的whl文件。

2.2 模型文件准备

需要下载以下预训练模型文件:

  1. dlib人脸关键点检测器:shape_predictor_68_face_landmarks.dat
  2. OpenCV人脸检测模型:opencv_face_detector.pbtxt和opencv_face_detector_uint8.pb
  3. 年龄识别模型:deploy_age.prototxt和age_net.caffemodel
  4. 性别识别模型:deploy_gender.prototxt和gender_net.caffemodel

建议新建一个 models 目录存放这些文件,保持项目结构清晰。

2.3 开发工具选择

推荐使用VS Code或PyCharm作为IDE,它们对Python和OpenCV的支持都很完善。调试时建议:

  • 使用IPython交互式环境快速验证代码片段
  • 利用Matplotlib实时显示图像处理中间结果
  • 对关键变量添加assert检查确保数据合法性

3. 疲劳检测系统实现

3.1 核心算法原理

疲劳检测的核心指标是眼睛纵横比(Eye Aspect Ratio, EAR),其计算公式为:

EAR = (||p2-p6|| + ||p3-p5||) / (2 * ||p1-p4||)

其中p1-p6是眼睛周围的6个关键点(dlib的68点模型中36-41和42-47点)。当人眼闭合时,EAR值会显著下降。

经过大量实验验证,我们发现:

  • 正常人睁眼状态EAR值通常在0.3-0.4之间
  • 完全闭眼时EAR值会降至0.15以下
  • 设置阈值为0.3时,对亚洲人眼型检测效果最佳

3.2 完整实现代码

import numpy as np
import cv2
import dlib
from scipy.spatial import distance as dist

# 初始化参数
EYE_AR_THRESH = 0.3  # EAR阈值
EYE_AR_CONSEC_FRAMES = 30  # 连续帧数
COUNTER = 0  # 闭眼帧计数器
ALARM_ON = False  # 警报状态

def eye_aspect_ratio(eye):
    # 计算垂直眼间距
    A = dist.euclidean(eye[1], eye[5])
    B = dist.euclidean(eye[2], eye[4])
    # 计算水平眼间距
    C = dist.euclidean(eye[0], eye[3])
    # 计算EAR
    ear = (A + B) / (2.0 * C)
    return ear

# 初始化dlib检测器
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("models/shape_predictor_68_face_landmarks.dat")

# 获取视频流
cap = cv2.VideoCapture(0)
while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    faces = detector(gray, 0)
    
    for face in faces:
        shape = predictor(gray, face)
        shape = np.array([(shape.part(i).x, shape.part(i).y) for i in range(68)])
        
        leftEye = shape[42:48]
        rightEye = shape[36:42]
        
        leftEAR = eye_aspect_ratio(leftEye)
        rightEAR = eye_aspect_ratio(rightEye)
        ear = (leftEAR + rightEAR) / 2.0
        
        # 绘制眼睛轮廓
        leftEyeHull = cv2.convexHull(leftEye)
        rightEyeHull = cv2.convexHull(rightEye)
        cv2.drawContours(frame, [leftEyeHull], -1, (0, 255, 0), 1)
        cv2.drawContours(frame, [rightEyeHull], -1, (0, 255, 0), 1)
        
        # 疲劳判断逻辑
        if ear < EYE_AR_THRESH:
            COUNTER += 1
            if COUNTER >= EYE_AR_CONSEC_FRAMES:
                if not ALARM_ON:
                    ALARM_ON = True
                    cv2.putText(frame, "FATIGUE ALERT!", (10, 30),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
        else:
            COUNTER = 0
            ALARM_ON = False
        
        # 显示EAR值
        cv2.putText(frame, "EAR: {:.2f}".format(ear), (300, 30),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
    
    cv2.imshow("Frame", frame)
    if cv2.waitKey(1) == 27:
        break

cap.release()
cv2.destroyAllWindows()

3.3 关键实现细节

  1. 多帧验证机制 :设置连续30帧(约1秒)EAR低于阈值才触发警报,避免瞬时眨眼导致的误报。

  2. 双眼EAR平均值 :同时计算左右眼的EAR并取平均,提高检测的鲁棒性。实测发现单眼检测在侧脸情况下容易失效。

  3. 动态阈值调整 :对于戴眼镜的用户,建议将阈值下调至0.25-0.28范围,可以减少镜片反光带来的干扰。

  4. 性能优化

    • 将图像转为灰度处理,减少计算量
    • 限制检测区域,避免全图扫描
    • 使用dlib的HOG检测器而非CNN,平衡精度和速度

4. 年龄性别识别系统实现

4.1 模型架构解析

我们使用的年龄性别识别模型基于Levi等人的研究,网络结构如下:

输入层 (227x227x3)
↓
卷积层 (96个7x7滤波器, stride=4)
↓
最大池化 (3x3, stride=2)
↓
局部响应归一化
↓
卷积层 (256个5x5滤波器)
↓
(重复池化和归一化)
↓
全连接层 (512神经元)
↓
Dropout (0.5)
↓
输出层 (年龄:8类 / 性别:2类)

模型在Adience数据集上训练,该数据集包含26k张人脸图像,覆盖不同年龄、性别和种族。

4.2 完整实现代码

import cv2
import numpy as np

# 模型路径
faceProto = "models/opencv_face_detector.pbtxt"
faceModel = "models/opencv_face_detector_uint8.pb"
ageProto = "models/deploy_age.prototxt"
ageModel = "models/age_net.caffemodel"
genderProto = "models/deploy_gender.prototxt"
genderModel = "models/gender_net.caffemodel"

# 加载模型
faceNet = cv2.dnn.readNet(faceModel, faceProto)
ageNet = cv2.dnn.readNet(ageModel, ageProto)
genderNet = cv2.dnn.readNet(genderModel, genderProto)

# 标签定义
ageList = ['(0-2)', '(4-6)', '(8-12)', '(15-20)', '(25-32)', '(38-43)', '(48-53)', '(60-100)']
genderList = ['Male', 'Female']
padding = 20

def getFaceBox(net, frame, conf_threshold=0.7):
    frameCopy = frame.copy()
    frameHeight = frameCopy.shape[0]
    frameWidth = frameCopy.shape[1]
    blob = cv2.dnn.blobFromImage(frameCopy, 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 > conf_threshold:
            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(frameCopy, (x1, y1), (x2, y2), (0, 255, 0), int(round(frameHeight/150)), 8)
    
    return frameCopy, faceBoxes

# 视频流处理
cap = cv2.VideoCapture(0)
while cv2.waitKey(1) < 0:
    ret, frame = cap.read()
    if not ret:
        break
    
    frameFace, faceBoxes = getFaceBox(faceNet, frame)
    if not faceBoxes:
        continue
    
    for faceBox in faceBoxes:
        face = frame[max(0, faceBox[1]-padding):min(faceBox[3]+padding, frame.shape[0]-1),
                     max(0, faceBox[0]-padding):min(faceBox[2]+padding, frame.shape[1]-1)]
        
        blob = cv2.dnn.blobFromImage(face, 1.0, (227, 227), (78.4263377603, 87.7689143744, 114.895847746), swapRB=False)
        
        # 性别预测
        genderNet.setInput(blob)
        genderPreds = genderNet.forward()
        gender = genderList[genderPreds[0].argmax()]
        
        # 年龄预测
        ageNet.setInput(blob)
        agePreds = ageNet.forward()
        age = ageList[agePreds[0].argmax()]
        
        # 显示结果
        label = "{},{}".format(gender, age)
        cv2.putText(frameFace, label, (faceBox[0], faceBox[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2, cv2.LINE_AA)
    
    cv2.imshow("Age Gender Detection", frameFace)

cap.release()
cv2.destroyAllWindows()

4.3 关键实现细节

  1. 人脸检测优化

    • 使用OpenCV的DNN模块加载基于ResNet的人脸检测器
    • 设置置信度阈值为0.7,平衡召回率和准确率
    • 添加20像素的padding,确保完整捕获面部特征
  2. 预处理标准化

    • 图像缩放至227x227分辨率
    • 使用模型训练时的均值(78.426, 87.768, 114.895)进行归一化
    • 保持BGR通道顺序(Caffe模型标准)
  3. 后处理技巧

    • 对连续视频帧采用滑动平均滤波,减少预测结果抖动
    • 添加年龄区间的置信度显示,提高结果可信度
    • 对极端光照条件添加直方图均衡化预处理

5. 工程实践中的经验总结

5.1 疲劳检测系统的调优经验

  1. 光照适应

    • 在低光照环境下,建议先进行直方图均衡化处理
    • 使用自适应阈值替代固定EAR阈值
    • 添加红外摄像头支持,实现全天候监测
  2. 多角度适配

    • 对侧脸情况,采用3D人脸关键点检测
    • 动态调整EAR阈值,根据头部姿态自适应变化
    • 添加眉毛位置辅助判断,提高闭眼检测准确率
  3. 误报处理

    • 结合嘴巴开合状态综合判断
    • 添加头部姿态检测,排除低头造成的假性闭眼
    • 设置最小报警间隔,避免频繁误报

5.2 年龄性别识别的优化方向

  1. 模型微调

    • 在自己的数据集上fine-tune预训练模型
    • 调整全连接层维度,优化特征提取能力
    • 使用更大的输入分辨率(300x300)提升细节识别
  2. 多模型集成

    • 结合多个年龄估计模型的预测结果
    • 添加人脸质量评估模块,过滤低质量输入
    • 使用时序信息平滑视频流中的预测结果
  3. 业务适配

    • 根据具体场景调整年龄分段(如儿童/成人二分法)
    • 添加种族识别分支,提高跨人种准确率
    • 结合人脸属性(胡须、皱纹等)辅助判断

6. 常见问题与解决方案

6.1 疲劳检测常见问题

Q1:系统对戴眼镜用户检测不准

  • 解决方案:使用红外图像避开镜片反光;或先检测眼镜区域,对该区域EAR计算做特殊处理

Q2:快速眨眼导致误报警

  • 解决方案:调整连续帧阈值(建议30-50帧);添加眨眼模式识别,区分自然眨眼和疲劳闭眼

Q3:侧脸情况下检测失效

  • 解决方案:采用3D人脸关键点检测;或当侧脸角度大于30度时暂停检测

6.2 年龄性别识别常见问题

Q1:年龄预测结果跳跃严重

  • 解决方案:添加时序平滑滤波;显示年龄区间而非具体数值

Q2:对儿童和老人识别准确率低

  • 解决方案:使用专门针对这两个年龄段优化的模型;添加人脸关键点稠密度特征

Q3:不同人种间的识别偏差

  • 解决方案:使用平衡的多种族数据集训练;添加人种分类分支做后校准

7. 项目扩展与进阶方向

  1. 多模态融合

    • 结合头部姿态估计,提高疲劳判断准确率
    • 添加心率估计(通过面部视频)作为辅助指标
    • 整合语音识别,检测打哈欠等声音特征
  2. 嵌入式部署

    • 使用TensorRT优化模型,在Jetson等边缘设备部署
    • 量化模型到INT8精度,提升推理速度
    • 开发C++版本,优化实时性能
  3. 云端服务化

    • 封装为RESTful API服务
    • 添加用户管理和数据分析功能
    • 实现分布式视频流处理架构

在实际项目中,我们通常会将这两个功能整合到一个完整的智能监控系统中,结合业务逻辑实现更复杂的应用场景。比如在驾驶员监控系统中,疲劳检测可以与车道偏离预警、前车碰撞预警等功能联动,形成全方位的安全防护体系。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐