使用 C++ 和 OpenCV 构建驾驶员疲劳检测软件
重要声明: 本文所描述的软件是一个概念验证的原型,绝对不能用作现实世界中的安全系统。真正的车载安全系统需要经过大量的测试、具备冗余设计并通过专业认证,以确保其绝对可靠。
驾驶疲劳是全球范围内引发交通事故的主要原因之一。当驾驶员感到困倦时,他们的反应时间会变慢,决策能力会下降,而在方向盘后睡着的风险则会急剧增加。为了解决这一关键问题,计算机视觉技术提供了一个充满希望的解决方案。通过使用摄像头实时监控驾驶员,我们可以开发算法来检测疲劳迹象,并及时发出警报。
本文将指导您完成一个使用 C++ 和流行的计算机视觉库 OpenCV 开发疲劳驾驶检测软件的全过程。我们将重点关注从面部特征中提取最常用且最可靠的指标:眼部纵横比 (EAR) 用于检测长时间闭眼和眨眼,嘴部纵横比 (MAR) 用于检测哈欠,以及对头部姿态的监控。
核心概念:如何检测疲劳?
我们的方法基于对三个核心疲劳指标的分析:
眼部闭合 (EAR): 这是最可靠的疲劳指标。当人变得昏昏欲睡时,他们的眨眼会变得更慢、持续时间更长。我们将使用一个称为 眼部纵横比 (Eye Aspect Ratio, EAR) 的度量标准来量化眼睛的睁开程度。EAR 是眼睑之间垂直距离与眼睛水平距离的比值。当眼睛睁开时,这个比值相对恒定,但当眼睛闭合时,它会迅速下降到接近零。通过持续监控 EAR,我们可以检测到驾驶员的眼睛是否长时间闭合。
打哈欠 (MAR): 打哈欠是另一个明显的疲劳迹象。与 EAR 类似,我们可以计算 嘴部纵横比 (Mouth Aspect Ratio, MAR) 来衡量嘴巴的张开程度。MAR 的显著增加可以标志着一次哈欠的发生。
头部姿态: 低头是极度困倦的另一个标志。通过追踪面部关键点的移动,可以估算头部的姿态。当检测到头部长时间处于前倾或侧倾状态时,可以将其作为疲劳的辅助判断依据。
环境与依赖配置
在开始编码之前,请确保您已经配置好了 C++ 开发环境,并安装了以下库:
- OpenCV: 开源计算机视觉库。您可以从 OpenCV 官网 下载。请确保为其正确配置了您的 C++ 编译器(如 g++, MinGW, MSVC)。
- Dlib: 一个包含机器学习算法的现代 C++ 工具包。我们将使用它进行人脸检测和面部关键点预测。请从 Dlib 官网 下载。
- 面部关键点预测模型: 您需要一个预训练模型来检测面部关键点。Dlib 提供了一个优秀的68点模型,您可以从这里下载:shape_predictor_68_face_landmarks.dat。下载后解压,并将
.dat
文件放置在您的项目目录中。
项目结构
您的项目目录结构应如下所示:
fatigue_detection/
|-- main.cpp
`-- shape_predictor_68_face_landmarks.dat
C++/OpenCV 实现步骤详解
现在,让我们一步步编写 main.cpp
文件中的代码。
1. 包含头文件与初始化
首先,我们需要包含 OpenCV 和 Dlib 的必要头文件,并初始化检测器和预测器。
#include <opencv2/opencv.hpp>
#include <dlib/opencv.h>
#include <dlib/image_processing/frontal_face_detector.h>
#include <dlib/image_processing/render_face_detections.h>
#include <dlib/image_processing.h>
#include <iostream>
using namespace cv;
using namespace std;
using namespace dlib;
// 计算两点之间的欧几里得距离
double euclidean_dist(dlib::point p1, dlib::point p2) {
return sqrt(pow(p1.x() - p2.x(), 2) + pow(p1.y() - p2.y(), 2));
}
// 计算眼部纵横比 (EAR)
double get_ear(const std::vector<dlib::point>& eye_landmarks) {
// 计算垂直距离
double vert_dist1 = euclidean_dist(eye_landmarks[1], eye_landmarks[5]);
double vert_dist2 = euclidean_dist(eye_landmarks[2], eye_landmarks[4]);
// 计算水平距离
double horz_dist = euclidean_dist(eye_landmarks[0], eye_landmarks[3]);
// 计算EAR
double ear = (vert_dist1 + vert_dist2) / (2.0 * horz_dist);
return ear;
}
// 计算嘴部纵横比 (MAR)
double get_mar(const std::vector<dlib::point>& mouth_landmarks) {
// 计算垂直距离 (点62到点66)
double vert_dist = euclidean_dist(mouth_landmarks[3], mouth_landmarks[9]);
// 计算水平距离 (点60到点64)
double horz_dist = euclidean_dist(mouth_landmarks[0], mouth_landmarks[6]);
// 计算MAR
double mar = vert_dist / horz_dist;
return mar;
}
2. 主函数 main()
主函数将处理视频流,执行检测,并显示结果。
int main() {
try {
// 初始化摄像头
VideoCapture cap(0);
if (!cap.isOpened()) {
cerr << "错误: 无法打开摄像头。" << endl;
return -1;
}
// 加载Dlib的人脸检测器
frontal_face_detector detector = get_frontal_face_detector();
// 加载形状预测器 (面部关键点)
shape_predictor sp;
deserialize("shape_predictor_68_face_landmarks.dat") >> sp;
// 定义阈值和计数器
const double EAR_THRESH = 0.21; // EAR阈值
const int EAR_CONSEC_FRAMES = 20; // 连续帧数,眼睛必须低于阈值
const double MAR_THRESH = 0.5; // MAR阈值
const int MAR_CONSEC_FRAMES = 15; // 连续帧数,嘴巴必须张开
int ear_counter = 0;
int mar_counter = 0;
bool drowsiness_alert = false;
bool yawn_alert = false;
cout << "疲劳监测已启动... 按 'q' 键退出。" << endl;
// 主循环,处理来自摄像头的每一帧
while (true) {
Mat frame;
cap >> frame;
if (frame.empty()) {
break;
}
// 将OpenCV帧转换为Dlib图像格式
cv_image<bgr_pixel> cimg(frame);
// 在图像中检测人脸
std::vector<dlib::rectangle> faces = detector(cimg);
// 遍历每个检测到的人脸
for (const auto& face : faces) {
// 获取面部关键点
full_object_detection shape = sp(cimg, face);
// 提取眼睛和嘴巴的关键点
std::vector<dlib::point> left_eye_landmarks;
std::vector<dlib::point> right_eye_landmarks;
std::vector<dlib::point> mouth_landmarks;
// 68个面部关键点的索引
// 左眼: 36-41
for (int i = 36; i <= 41; ++i) left_eye_landmarks.push_back(shape.part(i));
// 右眼: 42-47
for (int i = 42; i <= 47; ++i) right_eye_landmarks.push_back(shape.part(i));
// 嘴巴内部轮廓: 60-67
for (int i = 60; i <= 67; ++i) mouth_landmarks.push_back(shape.part(i));
// 计算双眼的平均EAR
double left_ear = get_ear(left_eye_landmarks);
double right_ear = get_ear(right_eye_landmarks);
double avg_ear = (left_ear + right_ear) / 2.0;
// 计算MAR
double mar = get_mar(mouth_landmarks);
// 为了可视化,在帧上绘制关键点
for (unsigned long i = 0; i < shape.num_parts(); ++i) {
circle(frame, Point(shape.part(i).x(), shape.part(i).y()), 2, Scalar(0, 255, 0), -1);
}
// 基于EAR检查是否瞌睡
if (avg_ear < EAR_THRESH) {
ear_counter++;
if (ear_counter >= EAR_CONSEC_FRAMES) {
if (!drowsiness_alert) {
drowsiness_alert = true;
// 在帧上显示警报
putText(frame, "!!! KUNLENG JINGGAO !!!", Point(10, 30),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 0, 255), 2);
}
}
} else {
ear_counter = 0;
drowsiness_alert = false;
}
// 基于MAR检查是否打哈欠
if (mar > MAR_THRESH) {
mar_counter++;
if (mar_counter >= MAR_CONSEC_FRAMES) {
if(!yawn_alert) {
yawn_alert = true;
// 显示哈欠警报
putText(frame, "!!! DAHAQIAN JINGGAO !!!", Point(10, 60),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 255), 2);
}
}
} else {
mar_counter = 0;
yawn_alert = false;
}
// 在帧上显示EAR和MAR的值
putText(frame, "EAR: " + to_string(avg_ear), Point(frame.cols - 200, 30),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(255, 0, 0), 2);
putText(frame, "MAR: " + to_string(mar), Point(frame.cols - 200, 60),
FONT_HERSHEY_SIMPLEX, 0.7, Scalar(255, 0, 0), 2);
}
// 显示处理后的帧
imshow("Fatigue Detection", frame);
// 如果按下 'q' 键,则退出循环
if (waitKey(1) == 'q') {
break;
}
}
} catch (const exception& e) {
cerr << "\n程序异常: " << e.what() << endl;
return -1;
}
return 0;
}
代码解析
- 初始化: 代码首先初始化摄像头捕获。然后,它从磁盘加载 Dlib 的
frontal_face_detector
(人脸检测器)和shape_predictor
(关键点预测模型)。 - 阈值定义:
EAR_THRESH
: 眼部纵横比的阈值。如果 EAR 低于此值,我们认为眼睛是闭合的。这个值可能需要根据不同的人、光照和摄像头位置进行微调。一个好的初始值在 0.2 到 0.3 之间。EAR_CONSEC_FRAMES
: EAR 必须连续低于阈值的帧数,才会触发瞌睡警报。这有助于避免因正常眨眼而产生的误报。MAR_THRESH
和MAR_CONSEC_FRAMES
: 用于哈欠检测的类似阈值和帧计数器。
- 帧处理循环:
- 捕获帧: 从摄像头获取一帧图像。
- 人脸检测: 使用 Dlib 的人脸检测器在帧中定位所有的人脸。
- 关键点预测: 对于每个检测到的人脸,调用形状预测器来定位68个面部关键点。
- 提取关键点: 根据68点模型的标准索引,提取左眼、右眼和嘴巴的坐标。
- 计算EAR和MAR: 调用我们自定义的
get_ear
和get_mar
函数来计算当前帧的指标。 - 检查疲劳状态:
- EAR 的逻辑检查平均 EAR 是否低于
EAR_THRESH
。如果是,则增加计数器ear_counter
。如果计数器达到EAR_CONSEC_FRAMES
,则显示瞌睡警报。如果 EAR 恢复到阈值以上,则重置计数器。 - 对 MAR 应用类似的逻辑来检测哈欠。
- EAR 的逻辑检查平均 EAR 是否低于
- 可视化: 将当前的 EAR 和 MAR 值以及任何警报信息绘制到帧上,以提供视觉反馈。关键点也以绿色小圆圈的形式绘制出来。
- 显示帧: 在窗口中显示带有注释的帧。
- 退出: 当用户按下 ‘q’ 键时,循环终止。
编译与运行
您在编译时需要链接 OpenCV 和 Dlib 库。一个在 Linux 上使用 g++ 的示例编译命令可能如下所示:
g++ main.cpp -o fatigue_detector `pkg-config --cflags --libs opencv4 dlib-1` -lpthread
注意: pkg-config
是管理编译标志的推荐方式。如果未安装,您需要手动指定包含路径和库链接,例如:
g++ main.cpp -o fatigue_detector -I/path/to/dlib/include -I/path/to/opencv/include -L/path/to/dlib/lib -L/path/to/opencv/lib -lopencv_core -lopencv_highgui -lopencv_videoio -lopencv_imgproc -ldlib -lpthread
编译成功后,运行程序:
./fatigue_detector
一个窗口将会弹出,显示您的摄像头画面。尝试长时间闭上眼睛或张大嘴巴,看看警报是否会触发。
改进方向与未来展望
这个软件提供了一个基础框架。对于一个更强大的系统,可以考虑以下几点:
- 校准: EAR 和 MAR 的基准值因人而异。一个更高级的系统可以从一个校准阶段开始,为每个驾驶员确定个性化的基准阈值。
- 头部姿态估计: 使用
cv::solvePnP
来实现头部姿态估计算法。通过跟踪头部的旋转,您可以检测到驾驶员是否点头,这是一个强烈的疲劳信号。 - 光照鲁棒性: 在光照条件不佳的情况下(例如夜晚),性能会显著下降。使用红外摄像头可以极大地提高可靠性。
- 警报集成: 系统可以不仅仅是显示文本,还可以连接到触发声音警报或触觉警报(如座椅振动),以更有效地吸引驾驶员的注意。
- 性能优化: 对于嵌入式系统,代码的性能优化至关重要。这可能包括使用更轻量级的人脸检测器(如OpenCV的Haar级联分类器,尽管精度较低)或加速图像处理。
结论
在本文中,我们使用 C++、OpenCV 和 Dlib 构建了一个功能性的软件,用于实时检测驾驶员的疲劳状态。通过分析如眼部纵横比和嘴部纵横比等指标,我们创建了一个能够有效预防事故的预警系统。尽管此实现作为一个优秀的概念验证,但从原型到可部署的安全系统还有很长的路要走,这需要精心的工程设计、严格的测试以及对各种现实世界场景的考量。然而,对于任何对计算机视觉在安全领域的实际应用感兴趣的人来说,这个项目都是一个绝佳的起点。