作为一名计算机视觉开发者,我经常需要处理各种实时视频分析任务。疲劳检测系统是一个特别实用的项目,它不仅涵盖了人脸检测、关键点定位等基础技术,还能直接应用于驾驶员监控、课堂注意力分析等实际场景。这个项目最吸引我的地方在于,它用简单的几何计算(眼睛纵横比)就实现了看似复杂的功能,完美诠释了"简单即美"的工程哲学。
在开始编码前,我们需要明确几个核心概念:
这个系统的典型应用场景包括:
提示:实际部署时需要考虑光照条件、摄像头角度等因素,这些都会影响检测效果。建议先在受控环境下测试,再逐步适应复杂场景。
在众多计算机视觉库中,我选择dlib和OpenCV的组合主要基于以下考量:
dlib的优势:
OpenCV的作用:
对比其他方案:
推荐使用Python 3.8+环境,太新的Python版本可能会遇到库兼容性问题。以下是创建虚拟环境的步骤:
bash复制# 创建虚拟环境
python -m venv fatigue_detection
source fatigue_detection/bin/activate # Linux/macOS
fatigue_detection\Scripts\activate # Windows
# 安装核心依赖
pip install numpy==1.21.5 opencv-python==4.5.5.64 dlib==19.24.2 scikit-learn==1.0.2 pillow==9.0.1
dlib的安装经常是新手遇到的第一个坎。根据我的经验,不同平台的最佳安装方式如下:
Windows系统:
bash复制pip install https://files.pythonhosted.org/packages/fr/.../dlib-19.24.2-cp38-cp38-win_amd64.whl
macOS/Linux:
bash复制# 先安装依赖
brew install cmake # macOS
sudo apt-get install cmake libboost-all-dev # Ubuntu
# 然后pip安装
pip install dlib
dlib需要预训练的shape predictor模型,官方提供了几种不同精度的模型。对于疲劳检测,68点模型是最佳选择:
下载模型文件:
bash复制wget http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2
bunzip2 shape_predictor_68_face_landmarks.dat.bz2
将解压后的.dat文件放在项目根目录,或者代码中指定完整路径
注意:模型文件约100MB,首次运行时会加载到内存,导致短暂延迟。在生产环境中,可以考虑预加载模型。
dlib使用的68点人脸关键点分布遵循iBUG 300-W标准,这是学术界广泛采用的基准。关键点编号和对应面部位置如下:
code复制0-16: 下巴轮廓
17-21: 右眉毛
22-26: 左眉毛
27-35: 鼻子
36-41: 右眼
42-47: 左眼
48-67: 嘴部
对于疲劳检测,我们主要关注眼睛区域(36-47点)。每个眼睛用6个点描述,分布位置为:
EAR公式的精妙之处在于它用简单的距离比值就能稳定反映眼睛状态。让我们拆解这个计算过程:
python复制def eye_aspect_ratio(eye):
# 计算垂直方向的两组距离
A = dist(eye[1], eye[5]) # 上眼睑中点与下眼睑中点的距离
B = dist(eye[2], eye[4]) # 上眼睑最高点与下眼睑最低点的距离
# 计算水平方向的距离
C = dist(eye[0], eye[3]) # 眼角间的距离
# 计算纵横比
ear = (A + B) / (2.0 * C)
return ear
这个设计的精妙之处在于:
实测EAR值范围:
单纯的EAR阈值判断会产生大量误报(比如眨眼)。我们需要引入状态机概念:
python复制# 状态变量
COUNTER = 0 # 连续闭眼帧数
ALARM_ON = False # 是否触发警报
# 主循环中的判断逻辑
if ear < EYE_AR_THRESH:
COUNTER += 1
if COUNTER >= EYE_AR_CONSEC_FRAMES and not ALARM_ON:
ALARM_ON = True
# 触发警报动作...
else:
if COUNTER >= EYE_AR_CONSEC_FRAMES:
# 恢复清醒状态
ALARM_ON = False
COUNTER = 0
这种设计带来了两个可调参数:
EYE_AR_THRESH:建议通过实验校准,通常0.2-0.3EYE_AR_CONSEC_FRAMES:取决于视频帧率,30fps下50帧≈1.67秒一个健壮的实时视频处理系统应该包含以下模块:
python复制# 初始化
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 降低分辨率提高速度
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
while True:
# 1. 帧捕获
ret, frame = cap.read()
if not ret:
break
# 2. 预处理
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray) # 增强对比度
# 3. 人脸检测
faces = detector(gray, 0)
# 4. 关键点检测与处理
for face in faces:
shape = predictor(gray, face)
# ...EAR计算逻辑...
# 5. 显示结果
cv2.imshow("Frame", frame)
if cv2.waitKey(1) == 27:
break
在树莓派等边缘设备上运行时,需要特别关注性能:
分辨率调整:
python复制cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
跳帧处理:
python复制frame_counter = 0
skip_frames = 2 # 每3帧处理1帧
if frame_counter % (skip_frames + 1) == 0:
# 处理逻辑...
frame_counter += 1
ROI(Region of Interest)检测:
python复制# 只在人脸可能出现的区域检测
roi = frame[y1:y2, x1:x2]
faces = detector(roi, 0)
多线程处理:
python复制from threading import Thread
class VideoStream:
def __init__(self, src=0):
self.stream = cv2.VideoCapture(src)
self.grabbed, self.frame = self.stream.read()
self.stopped = False
def start(self):
Thread(target=self.update, args=()).start()
return self
def update(self):
while not self.stopped:
self.grabbed, self.frame = self.stream.read()
OpenCV的putText不支持中文是个常见痛点。我推荐以下几种解决方案:
方案1:PIL桥接(如文中所示)
方案2:预渲染文字为图像
python复制def create_text_image(text, font_path, font_size, text_color, bg_color):
font = ImageFont.truetype(font_path, font_size)
size = font.getsize(text)
img = Image.new("RGB", size, bg_color)
draw = ImageDraw.Draw(img)
draw.text((0,0), text, font=font, fill=text_color)
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
# 预生成常用文字
warning_img = create_text_image("警告!", "simsun.ttc", 50, (0,0,255), (0,0,0))
方案3:使用freetype-py
python复制import freetype
def draw_text(image, pos, text, size, color):
face = freetype.Face("simsun.ttc")
face.set_char_size(size * 64)
pen = freetype.Vector()
pen.x = pos[0] * 64
pen.y = (image.shape[0] - pos[1]) * 64
for c in text:
face.load_char(c)
bitmap = face.glyph.bitmap
# 将bitmap绘制到image上...
return image
EAR阈值不是固定值,应该针对不同用户进行校准:
数据采集阶段:
阈值计算:
python复制# 假设我们收集到以下样本
open_eye_ears = [0.28, 0.31, 0.29, 0.27]
close_eye_ears = [0.12, 0.08, 0.15, 0.09]
threshold = (min(open_eye_ears) + max(close_eye_ears)) / 2
动态调整:
python复制# 运行时自适应
if current_ear > 0.25:
open_samples.append(current_ear)
if len(open_samples) > 30:
EYE_AR_THRESH = np.mean(open_samples) * 0.8
一个完整的评估应该包括:
| 指标 | 计算方法 | 目标值 |
|---|---|---|
| 准确率 | (TP+TN)/(TP+FP+TN+FN) | >90% |
| 召回率 | TP/(TP+FN) | >85% |
| 误报率 | FP/(FP+TN) | <5% |
| 延迟 | 处理一帧的平均时间 | <100ms |
其中:
不同光照条件下的EAR值会有波动,解决方案包括:
直方图均衡化:
python复制gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
自适应阈值:
python复制def dynamic_ear_threshold(ear_history):
return np.mean(ear_history) * 0.7
红外摄像头:在车载等专业场景,可以考虑使用红外摄像头消除光照影响
症状:
code复制RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat
解决方案:
可能原因:
调试步骤:
python复制for i, (x, y) in enumerate(shape):
cv2.circle(frame, (x, y), 1, (0, 255, 0), -1)
cv2.putText(frame, str(i), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1)
使用cProfile定位性能热点:
python复制import cProfile
def main():
# 你的主代码
if __name__ == "__main__":
cProfile.run('main()', sort='cumtime')
典型优化点:
结合更多生理指标提高准确性:
嘴部纵横比(MAR):
python复制def mouth_aspect_ratio(mouth):
# mouth是嘴部关键点(48-68)
A = dist(mouth[2], mouth[10]) # 上下唇距离
B = dist(mouth[4], mouth[8]) # 嘴角距离
return A / B
头部姿态估计:
python复制# 使用solvePnP计算头部姿态
image_points = np.array([shape[30], shape[8], shape[36], ...], dtype="double")
model_points = np.array([(0.0,0.0,0.0), (0.0,-330.0,-65.0), ...]) # 3D参考点
_, rotation, translation = cv2.solvePnP(model_points, image_points, camera_matrix, dist_coeffs)
PERCLOS指标:
传统方法结合深度学习可以进一步提升效果:
使用更精确的关键点检测模型:
端到端的疲劳检测模型:
python复制# 使用时间卷积网络(TCN)处理EAR序列
model = Sequential([
Conv1D(32, 5, activation='relu', input_shape=(None, 1)),
MaxPooling1D(2),
Conv1D(64, 5, activation='relu'),
GlobalMaxPooling1D(),
Dense(1, activation='sigmoid')
])
数据增强技巧:
将原型转化为实际产品需要考虑:
跨平台打包:
硬件加速:
python复制# 启用OpenCL加速
cv2.ocl.setUseOpenCL(True)
日志与监控:
用户界面优化:
这个项目最让我兴奋的是它的可扩展性。从最初的简单EAR检测,可以逐步演进为一个完整的行为分析系统。在实际开发中,我发现系统集成往往比算法本身更具挑战性,特别是在处理实时视频流时。建议新手先从静态图像开始调试算法,再逐步过渡到视频处理。