重要声明: 本文所描述的软件是一个概念验证的原型,绝对不能用作现实世界中的安全系统。真正的车载安全系统需要经过大量的测试、具备冗余设计并通过专业认证,以确保其绝对可靠。
驾驶疲劳是全球范围内引发交通事故的主要原因之一。当驾驶员感到困倦时,他们的反应时间会变慢,决策能力会下降,而在方向盘后睡着的风险则会急剧增加。为了解决这一关键问题,计算机视觉技术提供了一个充满希望的解决方案。通过使用摄像头实时监控驾驶员,我们可以开发算法来检测疲劳迹象,并及时发出警报。
本文将指导您完成一个使用 C++ 和流行的计算机视觉库 OpenCV 开发疲劳驾驶检测软件的全过程。我们将重点关注从面部特征中提取最常用且最可靠的指标:眼部纵横比 (EAR) 用于检测长时间闭眼和眨眼,嘴部纵横比 (MAR) 用于检测哈欠,以及对头部姿态的监控。
我们的方法基于对三个核心疲劳指标的分析:
眼部闭合 (EAR): 这是最可靠的疲劳指标。当人变得昏昏欲睡时,他们的眨眼会变得更慢、持续时间更长。我们将使用一个称为 眼部纵横比 (Eye Aspect Ratio, EAR) 的度量标准来量化眼睛的睁开程度。EAR 是眼睑之间垂直距离与眼睛水平距离的比值。当眼睛睁开时,这个比值相对恒定,但当眼睛闭合时,它会迅速下降到接近零。通过持续监控 EAR,我们可以检测到驾驶员的眼睛是否长时间闭合。
打哈欠 (MAR): 打哈欠是另一个明显的疲劳迹象。与 EAR 类似,我们可以计算 嘴部纵横比 (Mouth Aspect Ratio, MAR) 来衡量嘴巴的张开程度。MAR 的显著增加可以标志着一次哈欠的发生。
头部姿态: 低头是极度困倦的另一个标志。通过追踪面部关键点的移动,可以估算头部的姿态。当检测到头部长时间处于前倾或侧倾状态时,可以将其作为疲劳的辅助判断依据。
在开始编码之前,请确保您已经配置好了 C++ 开发环境,并安装了以下库:
.dat
文件放置在您的项目目录中。您的项目目录结构应如下所示:
fatigue_detection/
|-- main.cpp
`-- shape_predictor_68_face_landmarks.dat
现在,让我们一步步编写 main.cpp
文件中的代码。
首先,我们需要包含 OpenCV 和 Dlib 的必要头文件,并初始化检测器和预测器。
#include
#include
#include
#include
#include
#include
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;
}
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;
}
frontal_face_detector
(人脸检测器)和 shape_predictor
(关键点预测模型)。EAR_THRESH
: 眼部纵横比的阈值。如果 EAR 低于此值,我们认为眼睛是闭合的。这个值可能需要根据不同的人、光照和摄像头位置进行微调。一个好的初始值在 0.2 到 0.3 之间。EAR_CONSEC_FRAMES
: EAR 必须连续低于阈值的帧数,才会触发瞌睡警报。这有助于避免因正常眨眼而产生的误报。MAR_THRESH
和 MAR_CONSEC_FRAMES
: 用于哈欠检测的类似阈值和帧计数器。get_ear
和 get_mar
函数来计算当前帧的指标。EAR_THRESH
。如果是,则增加计数器 ear_counter
。如果计数器达到 EAR_CONSEC_FRAMES
,则显示瞌睡警报。如果 EAR 恢复到阈值以上,则重置计数器。您在编译时需要链接 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
一个窗口将会弹出,显示您的摄像头画面。尝试长时间闭上眼睛或张大嘴巴,看看警报是否会触发。
这个软件提供了一个基础框架。对于一个更强大的系统,可以考虑以下几点:
cv::solvePnP
来实现头部姿态估计算法。通过跟踪头部的旋转,您可以检测到驾驶员是否点头,这是一个强烈的疲劳信号。在本文中,我们使用 C++、OpenCV 和 Dlib 构建了一个功能性的软件,用于实时检测驾驶员的疲劳状态。通过分析如眼部纵横比和嘴部纵横比等指标,我们创建了一个能够有效预防事故的预警系统。尽管此实现作为一个优秀的概念验证,但从原型到可部署的安全系统还有很长的路要走,这需要精心的工程设计、严格的测试以及对各种现实世界场景的考量。然而,对于任何对计算机视觉在安全领域的实际应用感兴趣的人来说,这个项目都是一个绝佳的起点。