使用 C++ 和 OpenCV 构建驾驶员疲劳检测软件

发布于:2025-07-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

使用 C++ 和 OpenCV 构建驾驶员疲劳检测软件

重要声明: 本文所描述的软件是一个概念验证的原型,绝对不能用作现实世界中的安全系统。真正的车载安全系统需要经过大量的测试、具备冗余设计并通过专业认证,以确保其绝对可靠。

驾驶疲劳是全球范围内引发交通事故的主要原因之一。当驾驶员感到困倦时,他们的反应时间会变慢,决策能力会下降,而在方向盘后睡着的风险则会急剧增加。为了解决这一关键问题,计算机视觉技术提供了一个充满希望的解决方案。通过使用摄像头实时监控驾驶员,我们可以开发算法来检测疲劳迹象,并及时发出警报。

本文将指导您完成一个使用 C++ 和流行的计算机视觉库 OpenCV 开发疲劳驾驶检测软件的全过程。我们将重点关注从面部特征中提取最常用且最可靠的指标:眼部纵横比 (EAR) 用于检测长时间闭眼和眨眼,嘴部纵横比 (MAR) 用于检测哈欠,以及对头部姿态的监控。

核心概念:如何检测疲劳?

我们的方法基于对三个核心疲劳指标的分析:

  1. 眼部闭合 (EAR): 这是最可靠的疲劳指标。当人变得昏昏欲睡时,他们的眨眼会变得更慢、持续时间更长。我们将使用一个称为 眼部纵横比 (Eye Aspect Ratio, EAR) 的度量标准来量化眼睛的睁开程度。EAR 是眼睑之间垂直距离与眼睛水平距离的比值。当眼睛睁开时,这个比值相对恒定,但当眼睛闭合时,它会迅速下降到接近零。通过持续监控 EAR,我们可以检测到驾驶员的眼睛是否长时间闭合。

  2. 打哈欠 (MAR): 打哈欠是另一个明显的疲劳迹象。与 EAR 类似,我们可以计算 嘴部纵横比 (Mouth Aspect Ratio, MAR) 来衡量嘴巴的张开程度。MAR 的显著增加可以标志着一次哈欠的发生。

  3. 头部姿态: 低头是极度困倦的另一个标志。通过追踪面部关键点的移动,可以估算头部的姿态。当检测到头部长时间处于前倾或侧倾状态时,可以将其作为疲劳的辅助判断依据。

环境与依赖配置

在开始编码之前,请确保您已经配置好了 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_THRESHMAR_CONSEC_FRAMES: 用于哈欠检测的类似阈值和帧计数器。
  • 帧处理循环:
    1. 捕获帧: 从摄像头获取一帧图像。
    2. 人脸检测: 使用 Dlib 的人脸检测器在帧中定位所有的人脸。
    3. 关键点预测: 对于每个检测到的人脸,调用形状预测器来定位68个面部关键点。
    4. 提取关键点: 根据68点模型的标准索引,提取左眼、右眼和嘴巴的坐标。
    5. 计算EAR和MAR: 调用我们自定义的 get_earget_mar 函数来计算当前帧的指标。
    6. 检查疲劳状态:
      • EAR 的逻辑检查平均 EAR 是否低于 EAR_THRESH。如果是,则增加计数器 ear_counter。如果计数器达到 EAR_CONSEC_FRAMES,则显示瞌睡警报。如果 EAR 恢复到阈值以上,则重置计数器。
      • 对 MAR 应用类似的逻辑来检测哈欠。
    7. 可视化: 将当前的 EAR 和 MAR 值以及任何警报信息绘制到帧上,以提供视觉反馈。关键点也以绿色小圆圈的形式绘制出来。
    8. 显示帧: 在窗口中显示带有注释的帧。
    9. 退出: 当用户按下 ‘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 构建了一个功能性的软件,用于实时检测驾驶员的疲劳状态。通过分析如眼部纵横比和嘴部纵横比等指标,我们创建了一个能够有效预防事故的预警系统。尽管此实现作为一个优秀的概念验证,但从原型到可部署的安全系统还有很长的路要走,这需要精心的工程设计、严格的测试以及对各种现实世界场景的考量。然而,对于任何对计算机视觉在安全领域的实际应用感兴趣的人来说,这个项目都是一个绝佳的起点。