MediaPipe 系列 43:IMS DMS 架构——眼动追踪流水线完整实现

一、眼动追踪业务背景

1.1 为什么需要眼动追踪?

疲劳驾驶是交通安全的主要威胁:

  • 美国 NHTSA 统计:疲劳驾驶导致 6.9% 的致命交通事故
  • 欧盟研究:驾驶员连续驾驶 4小时 后,事故风险增加 2倍
  • Euro NCAP 2025+ 要求:DMS 必须检测疲劳状态并发出告警
  • 中国 GB 26149-2017:强制要求乘用车安装 DMS,具备疲劳监测功能

1.2 疲劳症状表现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌─────────────────────────────────────────────────────────────┐
│ 疲劳驾驶症状表现 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 视觉症状 │
│ ├── 眨眼频率异常(过快或过慢) │
│ ├── 眼睑闭合时间延长 │
│ ├── 眼球运动减少 │
│ ├── 眼球偏离前方 │
│ └── 瞳孔异常 │
│ │
│ 头部姿态 │
│ ├── 头部前倾 │
│ ├── 头部左右偏转 │
│ ├── 头部运动减少 │
│ └── 头部姿态不稳定 │
│ │
│ 行为表现 │
│ ├── 频繁打哈欠 │
│ ├── 呼吸变慢 │
│ ├── 嘴角下垂 │
│ └── 操作迟缓 │
│ │
│ 认知表现 │
│ ├── 注意力下降 │
│ ├── 反应变慢 │
│ ├── 记忆力减退 │
│ └── 情绪波动 │
│ │
└─────────────────────────────────────────────────────────────┘

1.3 PERCLOS 指标

PERCLOS(Percentage of Eyelid Closure over the Pupil) 是评估疲劳的核心指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────┐
│ PERCLOS 定义 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PERCLOS = (总闭眼时间 / 总观测时间) × 100% │
│ │
│ 示例: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 时间轴: 0s 1s 2s 3s 4s 5s │ │
│ │ 眼睑状态: ○ ○ ● ○ ● ○ │ │
│ │ 闭眼时长: 0s 0s 1s 0s 1s 0s │ │
│ │ 总闭眼: 2s │
│ │ 总时间: 5s │
│ │ PERCLOS: (2/5) × 100% = 40% │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 疲劳阈值: │
│ ├── 正常状态:PERCLOS < 15% │
│ ├── 轻度疲劳:15% ≤ PERCLOS < 30% │
│ ├── 中度疲劳:30% ≤ PERCLOS < 50% │
│ ├── 重度疲劳:PERCLOS ≥ 50% │
│ │
└─────────────────────────────────────────────────────────────┘

1.4 眨眼检测指标体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────┐
│ 眨眼检测指标体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 一级指标(直接观测) │
│ ├── Eye Aspect Ratio (EAR):眼睛纵横比 │
│ ├── Eye Closure:眼睛闭合程度 │
│ ├── Blink Duration:单次眨眼时长 │
│ └── Blink Frequency:眨眼频率(次/分钟) │
│ │
│ 二级指标(PERCLOS) │
│ ├── PERCLOS:闭眼比例 │
│ ├── PERCLOS_30s30秒内闭眼比例 │
│ └── PERCLOS_1m1分钟内闭眼比例 │
│ │
│ 三级指标(疲劳评估) │
│ ├── Fatigue Level:疲劳等级(0-3) │
│ ├── Alert Level:告警等级(无///高) │
│ └── Alert Decision:是否触发疲劳告警 │
│ │
└─────────────────────────────────────────────────────────────┘

二、眼睛关键点检测原理

2.1 Eye Aspect Ratio (EAR) 算法

EAR 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌─────────────────────────────────────────────────────────────┐
│ EAR 计算公式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ EAR = (||p2-p6|| + ||p3-p5||) / (2 × ||p1-p4||) │
│ │
│ 关键点定义(左眼): │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ p2 │ │
│ │ / \ │ │
│ │ / \ │ │
│ │ p1-----p4 │ │
│ │ \ / │ │
│ │ \ / │ │
│ │ p3 │ │
│ │ / \ │ │
│ │ / \ │ │
│ │ p0-----p5 │ │
│ │ │ │
│ │ p0: 上眼睑上角 │ │
│ │ p1: 上眼睑左角 │ │
│ │ p2: 下眼睑左角 │ │
│ │ p3: 下眼睑右角 │ │
│ │ p4: 上眼睑右角 │ │
│ │ p5: 下眼睑上角 │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
p2-p6: 左眼水平宽度 │
p3-p5: 左眼垂直高度 │
p1-p4: 左眼主对角线长度 │
│ │
│ 正常睁眼时:EAR ≈ 0.3-0.4
│ 闭合时:EAR ≈ 0.0-0.1
│ │
└─────────────────────────────────────────────────────────────┘

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// mediapipe/calculators/ims/eye_aspect_ratio_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_EYE_ASPECT_RATIO_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_EYE_ASPECT_RATIO_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/landmark.pb.h"
#include "mediapipe/calculators/ims/eye_aspect_ratio_options.pb.h"

namespace mediapipe {

// 眼睛纵横比
struct EyeAspectRatio {
float left_ear; // 左眼 EAR
float right_ear; // 右眼 EAR
float avg_ear; // 平均 EAR
float confidence;
int64_t timestamp;
};

class EyeAspectRatioCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc);

absl::Status Open(CalculatorContext* cc) override;
absl::Status Process(CalculatorContext* cc) override;

private:
// 计算单眼 EAR
float CalculateEAR(
const NormalizedLandmarkList& landmarks,
const std::vector<int>& upper_indices,
const std::vector<int>& lower_indices);

// 配置
int left_iris_center_;
int right_iris_center_;

// 阈值配置
float normal_ear_threshold_;
float closed_ear_threshold_;
};

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMS_EYE_ASPECT_RATIO_CALCULATOR_H_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// mediapipe/calculators/ims/eye_aspect_ratio_calculator.cc
#include "mediapipe/calculators/ims/eye_aspect_ratio_calculator.h"
#include "mediapipe/framework/port/logging.h"

namespace mediapipe {

using mediapipe::NormalizedLandmarkList;
using mediapipe::NormalizedLandmark;

absl::Status EyeAspectRatioCalculator::GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("LANDMARKS").Set<NormalizedLandmarkList>();

cc->Outputs().Tag("EAR").Set<EyeAspectRatio>();

cc->Options<EyeAspectRatioOptions>();

return absl::OkStatus();
}

absl::Status EyeAspectRatioCalculator::Open(CalculatorContext* cc) {
const auto& options = cc->Options<EyeAspectRatioOptions>();

left_iris_center_ = options.left_iris_center();
right_iris_center_ = options.right_iris_center();
normal_ear_threshold_ = options.normal_ear_threshold();
closed_ear_threshold_ = options.closed_ear_threshold();

LOG(INFO) << "EyeAspectRatioCalculator initialized";

return absl::OkStatus();
}

absl::Status EyeAspectRatioCalculator::Process(CalculatorContext* cc) {
if (cc->Inputs().Tag("LANDMARKS").IsEmpty()) {
return absl::OkStatus();
}

const auto& landmarks = cc->Inputs().Tag("LANDMARKS").Get<NormalizedLandmarkList>();

// 验证关键点数量
if (landmarks.landmark_size() < 478) {
LOG(WARNING) << "Insufficient landmarks for EAR calculation";
return absl::OkStatus();
}

// 左眼 EAR(使用标准 MediaPipe 眼睛关键点)
float left_ear = CalculateEAR(
landmarks,
{159, 145, 133, 153, 154, 145}, // 上眼睑
{33, 160, 158, 133, 153, 144} // 下眼睑
);

// 右眼 EAR
float right_ear = CalculateEAR(
landmarks,
{386, 374, 373, 380, 374, 380}, // 上眼睑
{263, 246, 161, 160, 159, 133} // 下眼睑
);

// 平均 EAR
float avg_ear = (left_ear + right_ear) / 2.0f;

// 置信度
float confidence = std::min({left_ear, right_ear, avg_ear});

// 输出
EyeAspectRatio ear;
ear.left_ear = left_ear;
ear.right_ear = right_ear;
ear.avg_ear = avg_ear;
ear.confidence = confidence;
ear.timestamp = cc->InputTimestamp().Value();

cc->Outputs().Tag("EAR").AddPacket(
MakePacket<EyeAspectRatio>(ear).At(cc->InputTimestamp()));

VLOG(1) << "EAR: left=" << left_ear << ", right=" << right_ear
<< ", avg=" << avg_ear;

return absl::OkStatus();
}

float EyeAspectRatioCalculator::CalculateEAR(
const NormalizedLandmarkList& landmarks,
const std::vector<int>& upper_indices,
const std::vector<int>& lower_indices) {

float numerator = 0.0f;
float denominator = 0.0f;

// 计算上眼睑两点距离
for (size_t i = 0; i < upper_indices.size() - 1; ++i) {
const auto& p1 = landmarks.landmark(upper_indices[i]);
const auto& p2 = landmarks.landmark(upper_indices[i + 1]);

numerator += std::sqrt(
std::pow(p2.x() - p1.x(), 2) + std::pow(p2.y() - p1.y(), 2)
);
}

// 计算下眼睑两点距离
for (size_t i = 0; i < lower_indices.size() - 1; ++i) {
const auto& p1 = landmarks.landmark(lower_indices[i]);
const auto& p2 = landmarks.landmark(lower_indices[i + 1]);

numerator += std::sqrt(
std::pow(p2.x() - p1.x(), 2) + std::pow(p2.y() - p1.y(), 2)
);
}

// 计算主对角线距离
for (size_t i = 0; i < upper_indices.size() - 1; ++i) {
const auto& p1 = landmarks.landmark(upper_indices[i]);
const auto& p2 = landmarks.landmark(lower_indices[i]);

denominator += std::sqrt(
std::pow(p2.x() - p1.x(), 2) + std::pow(p2.y() - p1.y(), 2)
);
}

// 避免除以零
if (denominator < 1e-6f) {
return 0.0f;
}

return numerator / denominator;
}

REGISTER_CALCULATOR(EyeAspectRatioCalculator);

} // namespace mediapipe

2.2 眨眼检测算法

眨眼定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────┐
│ 眨眼检测逻辑 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 眨眼判断流程: │
│ │
│ 1. 计算当前 EAR │
│ 2. 判断是否低于阈值 │
│ 3. 记录闭眼时间 │
│ 4. EAR 恢复到正常范围 │
│ 5. 记录睁眼时间 │
│ 6. 计算单次眨眼时长 │
│ 7. 判断是否为有效眨眼(时长 > 80ms) │
│ │
│ 有效眨眼定义: │
│ ├── 闭眼时间 > 80ms │
│ ├── 眨眼间隔 > 200ms(避免误检) │
│ └── EAR 恢复到正常范围(> 0.2) │
│ │
└─────────────────────────────────────────────────────────────┘

眨眼检测器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// mediapipe/calculators/ims/blinker_detector.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_BLINKER_DETECTOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_BLINKER_DETECTOR_H_

#include <deque>
#include <chrono>
#include <memory>

#include "mediapipe/calculators/ims/eye_aspect_ratio_calculator.h"

namespace mediapipe {

// 眨眼事件
struct BlinkEvent {
int64_t start_time; // 闭眼开始时间
int64_t end_time; // 睁眼结束时间
float duration_ms; // 眨眼时长(毫秒)
float left_ear; // 闭眼时左眼 EAR
float right_ear; // 闭眼时右眼 EAR
};

// 眨眼检测器
class BlinkerDetector {
public:
BlinkerDetector();

// 更新 EAR 并检测眨眼
void UpdateEAR(float ear, int64_t timestamp);

// 获取当前眨眼状态
bool IsBlinking() const;

// 获取当前眨眼时长
float GetCurrentBlinkDuration() const;

// 获取最近的眨眼事件
std::optional<BlinkEvent> GetLastBlink() const;

// 获取眨眼频率(次/分钟)
float GetBlinkFrequency() const;

// 重置检测器
void Reset();

private:
// 闭眼检测
void DetectBlink();

// 配置参数
static constexpr float kClosedEARThreshold = 0.2f;
static constexpr float kOpenEARThreshold = 0.25f;
static constexpr int64_t kMinBlinkDurationMs = 80;
static constexpr int64_t kMinBlinkIntervalMs = 200;

// 状态变量
float current_ear_;
bool is_blinking_;
int64_t blink_start_time_;
int64_t last_blink_time_;

// 眨眼历史
std::deque<BlinkEvent> blink_history_;
static constexpr int kMaxBlinkHistory = 60; // 1 分钟历史
};

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMS_BLINKER_DETECTOR_H_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// mediapipe/calculators/ims/blinker_detector.cc
#include "mediapipe/calculators/ims/blinker_detector.h"
#include "mediapipe/framework/port/logging.h"

namespace mediapipe {

BlinkerDetector::BlinkerDetector()
: current_ear_(0.4f),
is_blinking_(false),
blink_start_time_(0),
last_blink_time_(0) {
}

void BlinkerDetector::UpdateEAR(float ear, int64_t timestamp) {
// 记录当前 EAR
current_ear_ = ear;

// 检测眨眼
DetectBlink();
}

void BlinkerDetector::DetectBlink() {
// 判断是否开始闭眼
if (!is_blinking_ && current_ear_ < kClosedEARThreshold) {
is_blinking_ = true;
blink_start_time_ = GetCurrentTimestamp();
VLOG(2) << "Blink started, EAR=" << current_ear_;
}

// 判断是否结束闭眼
if (is_blinking_ && current_ear_ > kOpenEARThreshold) {
int64_t now = GetCurrentTimestamp();
int64_t duration_ms = now - blink_start_time_;

// 只有闭眼时间足够长才记录为有效眨眼
if (duration_ms >= kMinBlinkDurationMs) {
BlinkEvent blink;
blink.start_time = blink_start_time_;
blink.end_time = now;
blink.duration_ms = static_cast<float>(duration_ms);
blink.left_ear = current_ear_;
blink.right_ear = current_ear_;

// 检查眨眼间隔
if (now - last_blink_time_ >= kMinBlinkIntervalMs) {
blink_history_.push_back(blink);
last_blink_time_ = now;

// 限制历史长度
while (blink_history_.size() > kMaxBlinkHistory) {
blink_history_.pop_front();
}

VLOG(1) << "Blink detected: duration=" << duration_ms << "ms";
}
}

is_blinking_ = false;
}
}

bool BlinkerDetector::IsBlinking() const {
return is_blinking_;
}

float BlinkerDetector::GetCurrentBlinkDuration() const {
if (!is_blinking_) {
return 0.0f;
}

int64_t now = GetCurrentTimestamp();
int64_t duration_ms = now - blink_start_time_;
return static_cast<float>(duration_ms);
}

std::optional<BlinkEvent> BlinkerDetector::GetLastBlink() const {
if (blink_history_.empty()) {
return std::nullopt;
}
return blink_history_.back();
}

float BlinkerDetector::GetBlinkFrequency() const {
if (blink_history_.empty()) {
return 0.0f;
}

// 计算最近 1 分钟内的眨眼次数
int64_t now = GetCurrentTimestamp();
int blink_count = 0;

for (const auto& blink : blink_history_) {
if (now - blink.end_time <= 60000) { // 1 分钟窗口
blink_count++;
}
}

return static_cast<float>(blink_count);
}

void BlinkerDetector::Reset() {
current_ear_ = 0.4f;
is_blinking_ = false;
blink_start_time_ = 0;
last_blink_time_ = 0;
blink_history_.clear();
}

int64_t BlinkerDetector::GetCurrentTimestamp() {
// 使用系统时钟
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}

} // namespace mediapipe

三、眼瞌睡检测算法(PERCLOS)

3.1 PERCLOS 计算原理

PERCLOS 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────┐
│ PERCLOS 计算流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PERCLOS(t) = (总闭眼时间) / t │
│ │
│ 滑动窗口 PERCLOS: │
│ PERCLOS_30s = (30秒内闭眼总时长) / 30秒 │
│ PERCLOS_1m = (60秒内闭眼总时长) / 60秒 │
│ │
│ 计算步骤: │
1. 检测所有闭眼事件 │
2. 计算窗口内的闭眼总时长 │
3. 除以窗口长度 │
4. 转换为百分比 │
│ │
└─────────────────────────────────────────────────────────────┘

3.2 PERCLOS 计算器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// mediapipe/calculators/ims/perclos_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_PERCLOS_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_PERCLOS_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/calculators/ims/blinker_detector.h"
#include "mediapipe/calculators/ims/eye_aspect_ratio_calculator.h"
#include "mediapipe/calculators/ims/perclos_options.pb.h"

namespace mediapipe {

// PERCLOS 结果
struct PerclosResult {
float perclos_30s; // 30秒 PERCLOS
float perclos_1m; // 60秒 PERCLOS
float perclos_5m; // 5分钟 PERCLOS
float blink_frequency; // 眨眼频率(次/分钟)
float fatigue_level; // 疲劳等级(0-3)
float alert_level; // 告警等级(0-3)
int64_t timestamp;
};

class PerclosCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc);

absl::Status Open(CalculatorContext* cc) override;
absl::Status Process(CalculatorContext* cc) override;

private:
// 计算 PERCLOS
float CalculatePerclos(const std::deque<BlinkEvent>& blink_history, int64_t window_ms);

// 疲劳等级评估
void EvaluateFatigueLevel(float perclos);

// 告警等级评估
void EvaluateAlertLevel(float perclos);

// 眨眼频率计算
float CalculateBlinkFrequency(const std::deque<BlinkEvent>& blink_history);

// 配置
int64_t window_30s_;
int64_t window_1m_;
int64_t window_5m_;

// 历史记录
std::deque<BlinkEvent> blink_history_;
static constexpr int kMaxBlinkHistory = 300; // 5分钟历史(30fps)
};

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMS_PERCLOS_CALCULATOR_H_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// mediapipe/calculators/ims/perclos_calculator.cc
#include "mediapipe/calculators/ims/perclos_calculator.h"
#include "mediapipe/framework/port/logging.h"

namespace mediapipe {

absl::Status PerclosCalculator::GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("EAR").Set<EyeAspectRatio>();

cc->Outputs().Tag("PERCLOS_RESULT").Set<PerclosResult>();

cc->Options<PerclosOptions>();

return absl::OkStatus();
}

absl::Status PerclosCalculator::Open(CalculatorContext* cc) {
const auto& options = cc->Options<PerclosOptions>();

window_30s_ = options.window_30s_ms();
window_1m_ = options.window_1m_ms();
window_5m_ = options.window_5m_ms();

LOG(INFO) << "PerclosCalculator initialized, windows: "
<< window_30s_ << "ms, " << window_1m_ << "ms, " << window_5m_ << "ms";

return absl::OkStatus();
}

absl::Status PerclosCalculator::Process(CalculatorContext* cc) {
if (cc->Inputs().Tag("EAR").IsEmpty()) {
return absl::OkStatus();
}

const auto& ear = cc->Inputs().Tag("EAR").Get<EyeAspectRatio>();

// 创建临时眨眼事件
BlinkEvent blink;
blink.start_time = ear.timestamp;
blink.end_time = ear.timestamp;
blink.duration_ms = ear.confidence > 0.5f ? 0.0f : 1000.0f; // 简化处理
blink.left_ear = ear.left_ear;
blink.right_ear = ear.right_ear;

// 更新历史
blink_history_.push_back(blink);

// 限制历史长度
while (blink_history_.size() > kMaxBlinkHistory) {
blink_history_.pop_front();
}

// 计算 PERCLOS
float perclos_30s = CalculatePerclos(blink_history_, window_30s_);
float perclos_1m = CalculatePerclos(blink_history_, window_1m_);
float perclos_5m = CalculatePerclos(blink_history_, window_5m_);

// 计算眨眼频率
float blink_frequency = CalculateBlinkFrequency(blink_history_);

// 评估疲劳等级
float fatigue_level = 0.0f;
if (perclos_30s >= 0.5f) {
fatigue_level = 3.0f; // 重度疲劳
} else if (perclos_30s >= 0.3f) {
fatigue_level = 2.0f; // 中度疲劳
} else if (perclos_30s >= 0.15f) {
fatigue_level = 1.0f; // 轻度疲劳
}

// 评估告警等级
float alert_level = 0.0f;
if (perclos_30s >= 0.5f) {
alert_level = 3.0f; // 高风险
} else if (perclos_30s >= 0.3f) {
alert_level = 2.0f; // 中风险
} else if (perclos_30s >= 0.15f) {
alert_level = 1.0f; // 低风险
}

// 输出结果
PerclosResult result;
result.perclos_30s = perclos_30s;
result.perclos_1m = perclos_1m;
result.perclos_5m = perclos_5m;
result.blink_frequency = blink_frequency;
result.fatigue_level = fatigue_level;
result.alert_level = alert_level;
result.timestamp = cc->InputTimestamp().Value();

cc->Outputs().Tag("PERCLOS_RESULT").AddPacket(
MakePacket<PerclosResult>(result).At(cc->InputTimestamp()));

VLOG(1) << "Perclos: 30s=" << perclos_30s * 100 << "%, "
<< "1m=" << perclos_1m * 100 << "%, "
<< "5m=" << perclos_5m * 100 << "%, "
<< "frequency=" << blink_frequency << " bpm";

return absl::OkStatus();
}

float PerclosCalculator::CalculatePerclos(
const std::deque<BlinkEvent>& blink_history,
int64_t window_ms) {

if (blink_history.empty()) {
return 0.0f;
}

int64_t now = GetCurrentTimestamp();
int64_t total_closed_time = 0;
int64_t window_start = std::max(0LL, now - window_ms);

// 统计窗口内的闭眼时间
for (const auto& blink : blink_history) {
// 只统计窗口内的闭眼事件
if (blink.end_time > window_start && blink.start_time < now) {
int64_t closed_in_window = std::min(blink.end_time, now) -
std::max(blink.start_time, window_start);
total_closed_time += std::max(0LL, closed_in_window);
}
}

// 计算 PERCLOS
float perclos = static_cast<float>(total_closed_time) / window_ms;

return std::min(perclos, 1.0f); // 限制在 0-1 之间
}

void PerclosCalculator::EvaluateFatigueLevel(float perclos) {
// 由 Process() 调用,这里预留扩展
}

void PerclosCalculator::EvaluateAlertLevel(float perclos) {
// 由 Process() 调用,这里预留扩展
}

float PerclosCalculator::CalculateBlinkFrequency(
const std::deque<BlinkEvent>& blink_history) {

if (blink_history.empty()) {
return 0.0f;
}

int64_t now = GetCurrentTimestamp();
int blink_count = 0;

// 统计最近 1 分钟内的眨眼次数
for (const auto& blink : blink_history) {
if (now - blink.end_time <= 60000) {
blink_count++;
}
}

return static_cast<float>(blink_count);
}

int64_t PerclosCalculator::GetCurrentTimestamp() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}

REGISTER_CALCULATOR(PerclosCalculator);

} // namespace mediapipe

四、完整流水线架构

4.1 架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
┌─────────────────────────────────────────────────────────────────────────┐
│ IMS DMS 眼动追踪完整流水线 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 输入层 │
│ ┌─────────────┐ │
│ │ IR Camera │ → 640×480 @ 30fps │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ 检测层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Face Mesh │────▶│EAR Calculator│────▶│Blinker │ │
│ │(468点) │ │ │ │Detector │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Face Landmarks│ │EAR Value │ │Blink Events │ │
│ │[x,y,z]×468 │ │(left,right) │ │(duration) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 分析层 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PERCLOS Calculator │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │PERCLOS 30s │ │PERCLOS 1m │ │PERCLOS 5m │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │Fatigue │ │ │
│ │ │Evaluation │ │ │
│ │ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 输出层 │ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PerclosResult { │ │
│ │ perclos_30s: 0.35, │ │
│ │ perclos_1m: 0.32, │ │
│ │ perclos_5m: 0.28, │ │
│ │ blink_frequency: 18.5, │ │
│ │ fatigue_level: 2, │ │
│ │ alert_level: 2, │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

4.2 疲劳等级标准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────┐
│ 疲劳等级标准 │
├─────────────────────────────────────────────────────────────┤
│ │
│ PERCLOS 30秒阈值 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 等级 0 (正常) │ │
│ │ PERCLOS < 15% │ │
│ │ 行为:正常眨眼,注意力集中 │ │
│ │ 告警:无 │ │
│ │ │ │
│ │ 等级 1 (轻度疲劳) │ │
│ │ 15% ≤ PERCLOS < 30% │ │
│ │ 行为:眨眼频率略低,偶尔闭眼 │ │
│ │ 告警:低风险 │ │
│ │ │ │
│ │ 等级 2 (中度疲劳) │ │
│ │ 30% ≤ PERCLOS < 50% │ │
│ │ 行为:频繁闭眼,PERCLOS 较高 │ │
│ │ 告警:中风险 │ │
│ │ │ │
│ │ 等级 3 (重度疲劳) │ │
│ │ PERCLOS ≥ 50% │ │
│ │ 行为:长时间闭眼,接近睡着 │ │
│ │ 告警:高风险 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

五、完整 Graph 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# mediapipe/graphs/ims/dms_fatigue_graph.pbtxt

# ============== 输入输出定义 ==============
input_stream: "IR_IMAGE:ir_image"
input_stream: "TIMESTAMP:timestamp"

output_stream: "PERCLOS_RESULT:perclos_result"
output_stream: "FATIGUE_ALERT:fatigue_alert"

# ============== 1. Face Mesh ==============
node {
calculator: "FaceMeshCalculator"
input_stream: "IMAGE:ir_image"
output_stream: "LANDMARKS:face_landmarks"
options {
[mediapipe.FaceMeshOptions.ext] {
model_path: "/models/face_landmark.tflite"
enable_attention: true
enable_iris: true
}
}
}

# ============== 2. EAR 计算 ==============
node {
calculator: "EyeAspectRatioCalculator"
input_stream: "LANDMARKS:face_landmarks"
output_stream: "EAR:ear"
options {
[mediapipe.EyeAspectRatioOptions.ext] {
# 眼睛关键点索引
left_iris_center: 468
right_iris_center: 473
# 阈值
normal_ear_threshold: 0.25
closed_ear_threshold: 0.2
}
}
}

# ============== 3. 眨眼检测 ==============
node {
calculator: "BlinkDetectorCalculator"
input_stream: "EAR:ear"
output_stream: "BLINK_EVENT:blink_event"
options {
[mediapipe.BlinkDetectorOptions.ext] {
# 眨眼参数
min_blink_duration_ms: 80
min_blink_interval_ms: 200
closed_ear_threshold: 0.2
open_ear_threshold: 0.25
}
}
}

# ============== 4. PERCLOS 计算 ==============
node {
calculator: "PerclosCalculator"
input_stream: "BLINK_EVENT:blink_event"
output_stream: "PERCLOS_RESULT:perclos_result"
options {
[mediapipe.PerclosOptions.ext] {
# 窗口大小(毫秒)
window_30s_ms: 30000
window_1m_ms: 60000
window_5m_ms: 300000
}
}
}

# ============== 5. 疲劳评估 ==============
node {
calculator: "FatigueEvaluatorCalculator"
input_stream: "PERCLOS_RESULT:perclos_result"
output_stream: "FATIGUE_ALERT:fatigue_alert"
options {
[mediapipe.FatigueEvaluatorOptions.ext] {
# 疲劳阈值
normal_perclos_threshold: 0.15
mild_perclos_threshold: 0.30
moderate_perclos_threshold: 0.50

# 告警配置
alert_cooldown_seconds: 30
alert_level: 1 # 1: 低风险, 2: 中风险, 3: 高风险
}
}
}

六、核心 Calculator 实现

6.1 BlinkDetectorCalculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// mediapipe/calculators/ims/blink_detector_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_BLINK_DETECTOR_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_BLINK_DETECTOR_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/calculators/ims/blinker_detector.h"
#include "mediapipe/calculators/ims/eye_aspect_ratio_calculator.h"

namespace mediapipe {

// 眨眼事件
struct BlinkEvent {
int64_t start_time;
int64_t end_time;
float duration_ms;
float ear;
float confidence;
};

class BlinkDetectorCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc);

absl::Status Open(CalculatorContext* cc) override;
absl::Status Process(CalculatorContext* cc) override;
absl::Status Close(CalculatorContext* cc) override;

private:
// 更新检测器状态
void UpdateDetector(const EyeAspectRatio& ear);

// 配置
int64_t min_blink_duration_ms_;
int64_t min_blink_interval_ms_;
float closed_ear_threshold_;
float open_ear_threshold_;

// 检测器实例
std::unique_ptr<BlinkerDetector> blinker_detector_;

// 上一次输出时间(用于去重)
int64_t last_output_time_ = 0;
};

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMS_BLINK_DETECTOR_CALCULATOR_H_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// mediapipe/calculators/ims/blink_detector_calculator.cc
#include "mediapipe/calculators/ims/blink_detector_calculator.h"
#include "mediapipe/framework/port/logging.h"

namespace mediapipe {

absl::Status BlinkDetectorCalculator::GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("EAR").Set<EyeAspectRatio>();

cc->Outputs().Tag("BLINK_EVENT").Set<BlinkEvent>();

cc->Options<BlinkDetectorOptions>();

return absl::OkStatus();
}

absl::Status BlinkDetectorCalculator::Open(CalculatorContext* cc) {
const auto& options = cc->Options<BlinkDetectorOptions>();

min_blink_duration_ms_ = options.min_blink_duration_ms();
min_blink_interval_ms_ = options.min_blink_interval_ms();
closed_ear_threshold_ = options.closed_ear_threshold();
open_ear_threshold_ = options.open_ear_threshold();

blinker_detector_ = std::make_unique<BlinkerDetector>();

LOG(INFO) << "BlinkDetectorCalculator initialized";

return absl::OkStatus();
}

absl::Status BlinkDetectorCalculator::Process(CalculatorContext* cc) {
if (cc->Inputs().Tag("EAR").IsEmpty()) {
return absl::OkStatus();
}

const auto& ear = cc->Inputs().Tag("EAR").Get<EyeAspectRatio>();

// 更新检测器
blinker_detector_->UpdateEAR(ear.avg_ear, ear.timestamp);

// 检测是否发生眨眼
if (blinker_detector_->IsBlinking()) {
int64_t now = GetCurrentTimestamp();
int64_t duration_ms = blinker_detector_->GetCurrentBlinkDuration();

// 只输出有效的眨眼事件
if (duration_ms >= min_blink_duration_ms_) {
BlinkEvent blink;
blink.start_time = blinker_detector_->GetLastBlink()->start_time;
blink.end_time = now;
blink.duration_ms = duration_ms;
blink.ear = ear.avg_ear;
blink.confidence = ear.confidence;

// 避免重复输出
if (now - last_output_time_ >= min_blink_interval_ms_) {
cc->Outputs().Tag("BLINK_EVENT").AddPacket(
MakePacket<BlinkEvent>(blink).At(cc->InputTimestamp()));

last_output_time_ = now;

VLOG(1) << "Blink detected: duration=" << duration_ms << "ms";
}
}
}

return absl::OkStatus();
}

absl::Status BlinkDetectorCalculator::Close(CalculatorContext* cc) {
blinker_detector_.reset();
return absl::OkStatus();
}

int64_t BlinkDetectorCalculator::GetCurrentTimestamp() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}

REGISTER_CALCULATOR(BlinkDetectorCalculator);

} // namespace mediapipe

6.2 FatigueEvaluatorCalculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// mediapipe/calculators/ims/fatigue_evaluator_calculator.h
#ifndef MEDIAPIPE_CALCULATORS_IMS_FATIGUE_EVALUATOR_CALCULATOR_H_
#define MEDIAPIPE_CALCULATORS_IMS_FATIGUE_EVALUATOR_CALCULATOR_H_

#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/calculators/ims/perclos_calculator.h"
#include "mediapipe/calculators/ims/fatigue_evaluator_options.pb.h"

namespace mediapipe {

// 疲劳告警
struct FatigueAlert {
int alert_level; // 告警等级(0-3)
float perclos_30s; // 30秒 PERCLOS
float perclos_1m; // 60秒 PERCLOS
float fatigue_level; // 疲劳等级(0-3)
bool trigger_alert; // 是否触发告警
int64_t timestamp;
};

class FatigueEvaluatorCalculator : public CalculatorBase {
public:
static absl::Status GetContract(CalculatorContract* cc);

absl::Status Open(CalculatorContext* cc) override;
absl::Status Process(CalculatorContext* cc) override;
absl::Status Close(CalculatorContext* cc) override;

private:
// 评估疲劳等级
void EvaluateFatigue(const PerclosResult& perclos);

// 评估告警等级
void EvaluateAlert(const PerclosResult& perclos);

// 检查是否应该触发告警
bool ShouldTriggerAlert(int alert_level);

// 配置
float normal_perclos_threshold_;
float mild_perclos_threshold_;
float moderate_perclos_threshold_;
float severe_perclos_threshold_;
int64_t alert_cooldown_ms_;

// 告警状态
int64_t last_alert_time_;
int current_alert_level_;
};

} // namespace mediapipe

#endif // MEDIAPIPE_CALCULATORS_IMS_FATIGUE_EVALUATOR_CALCULATOR_H_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// mediapipe/calculators/ims/fatigue_evaluator_calculator.cc
#include "mediapipe/calculators/ims/fatigue_evaluator_calculator.h"
#include "mediapipe/framework/port/logging.h"

namespace mediapipe {

absl::Status FatigueEvaluatorCalculator::GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("PERCLOS_RESULT").Set<PerclosResult>();

cc->Outputs().Tag("FATIGUE_ALERT").Set<FatigueAlert>();

cc->Options<FatigueEvaluatorOptions>();

return absl::OkStatus();
}

absl::Status FatigueEvaluatorCalculator::Open(CalculatorContext* cc) {
const auto& options = cc->Options<FatigueEvaluatorOptions>();

normal_perclos_threshold_ = options.normal_perclos_threshold();
mild_perclos_threshold_ = options.mild_perclos_threshold();
moderate_perclos_threshold_ = options.moderate_perclos_threshold();
severe_perclos_threshold_ = options.severe_perclos_threshold();
alert_cooldown_ms_ = options.alert_cooldown_seconds() * 1000;

last_alert_time_ = 0;
current_alert_level_ = 0;

LOG(INFO) << "FatigueEvaluatorCalculator initialized";

return absl::OkStatus();
}

absl::Status FatigueEvaluatorCalculator::Process(CalculatorContext* cc) {
if (cc->Inputs().Tag("PERCLOS_RESULT").IsEmpty()) {
return absl::OkStatus();
}

const auto& perclos = cc->Inputs().Tag("PERCLOS_RESULT").Get<PerclosResult>();

// 评估疲劳等级
EvaluateFatigue(perclos);

// 评估告警等级
EvaluateAlert(perclos);

// 检查是否触发告警
if (ShouldTriggerAlert(current_alert_level_)) {
FatigueAlert alert;
alert.alert_level = current_alert_level_;
alert.perclos_30s = perclos.perclos_30s;
alert.perclos_1m = perclos.perclos_1m;
alert.fatigue_level = perclos.fatigue_level;
alert.trigger_alert = true;
alert.timestamp = cc->InputTimestamp().Value();

cc->Outputs().Tag("FATIGUE_ALERT").AddPacket(
MakePacket<FatigueAlert>(alert).At(cc->InputTimestamp()));

last_alert_time_ = cc->InputTimestamp().Value();

VLOG(1) << "Fatigue alert triggered: level=" << current_alert_level_
<< ", perclos_30s=" << perclos.perclos_30s * 100 << "%";
}

return absl::OkStatus();
}

absl::Status FatigueEvaluatorCalculator::Close(CalculatorContext* cc) {
return absl::OkStatus();
}

void FatigueEvaluatorCalculator::EvaluateFatigue(const PerclosResult& perclos) {
// 疲劳等级基于 PERCLOS 30秒
if (perclos.perclos_30s >= severe_perclos_threshold_) {
perclos.fatigue_level = 3.0f; // 重度疲劳
} else if (perclos.perclos_30s >= moderate_perclos_threshold_) {
perclos.fatigue_level = 2.0f; // 中度疲劳
} else if (perclos.perclos_30s >= mild_perclos_threshold_) {
perclos.fatigue_level = 1.0f; // 轻度疲劳
} else {
perclos.fatigue_level = 0.0f; // 正常
}
}

void FatigueEvaluatorCalculator::EvaluateAlert(const PerclosResult& perclos) {
// 告警等级
if (perclos.perclos_30s >= severe_perclos_threshold_) {
current_alert_level_ = 3; // 高风险
} else if (perclos.perclos_30s >= moderate_perclos_threshold_) {
current_alert_level_ = 2; // 中风险
} else if (perclos.perclos_30s >= mild_perclos_threshold_) {
current_alert_level_ = 1; // 低风险
} else {
current_alert_level_ = 0; // 无风险
}
}

bool FatigueEvaluatorCalculator::ShouldTriggerAlert(int alert_level) {
// 没有告警
if (alert_level == 0) {
return false;
}

// 冷却时间未过
int64_t now = GetCurrentTimestamp();
if (now - last_alert_time_ < alert_cooldown_ms_) {
return false;
}

return true;
}

int64_t FatigueEvaluatorCalculator::GetCurrentTimestamp() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}

REGISTER_CALCULATOR(FatigueEvaluatorCalculator);

} // namespace mediapipe

七、测试与验证

7.1 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include "mediapipe/calculators/ims/blinker_detector_test.cc"

TEST(BlinkerDetectorTest, DetectsNormalBlink) {
BlinkerDetector detector;

// 模拟正常眨眼
detector.UpdateEAR(0.35, 1000); // 睁眼
detector.UpdateEAR(0.15, 1200); // 闭眼
detector.UpdateEAR(0.35, 1300); // 睁眼

auto last_blink = detector.GetLastBlink();
ASSERT_TRUE(last_blink.has_value());
EXPECT_GT(last_blink->duration_ms, 80); // 超过最小阈值
EXPECT_LT(last_blink->duration_ms, 500); // 合理时长
}

TEST(BlinkerDetectorTest, FiltersShortBlinks) {
BlinkerDetector detector;

// 模拟过短的眨眼(可能是噪声)
detector.UpdateEAR(0.35, 1000);
detector.UpdateEAR(0.15, 1010); // 仅闭眼 10ms
detector.UpdateEAR(0.35, 1020);

auto last_blink = detector.GetLastBlink();
EXPECT_FALSE(last_blink.has_value()); // 不记录
}

TEST(BlinkerDetectorTest, CalculatesPerclos) {
// 模拟多次眨眼
std::vector<int64_t> timestamps = {1000, 1200, 1400, 1600, 1800};
std::vector<float> ears = {0.35, 0.15, 0.35, 0.12, 0.35};

// 这里需要完整模拟 PerclosCalculator
// 略
}

TEST(FatigueEvaluatorTest, EvaluatesFatigueLevel) {
PerclosResult perclos;
perclos.perclos_30s = 0.45; // 中度疲劳

FatigueEvaluator evaluator;
evaluator.EvaluateFatigue(perclos);

EXPECT_FLOAT_EQ(perclos.fatigue_level, 2.0f); // 中度疲劳
}

7.2 性能测试

1
2
3
4
5
6
7
8
9
TEST(FatigueGraphPerformanceTest, Processes30FPS) {
// 测试流水线能否处理 30fps 输入
// 略
}

TEST(EARPerformanceTest, CalculatesEARUnder1ms) {
// 测试 EAR 计算性能
// 略
}

八、调试与优化经验

8.1 常见问题

问题 原因 解决方案
EAR 频繁抖动 检测噪声 使用平滑滤波
误检眨眼 阈值过小 调整 min_blink_duration_ms
PERCLOS 计算错误 历史窗口未正确更新 检查时间戳处理
疲劳等级不准确 阈值不合适 根据实际数据调整

8.2 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 优化 1: 使用固定大小队列
std::deque<BlinkEvent> blink_history_;
static constexpr int kMaxBlinkHistory = 300; // 5分钟历史

// 优化 2: 使用时间戳快速过滤
int64_t now = GetCurrentTimestamp();
int64_t window_start = now - window_ms;

// 优化 3: 避免重复计算
float CalculateEAR(...) {
static float last_ear = 0.0f;
if (std::abs(ear - last_ear) < 0.01f) {
return last_ear; // 返回上一次结果
}
last_ear = ear;
// ... 计算逻辑
}

8.3 阈值调优建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────┐
│ 阈值调优建议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ EAR 阈值: │
│ ├── 正常睁眼:0.25-0.35 │
│ ├── 闭眼阈值:0.2-0.25 │
│ ├── 最小眨眼时长:80-120ms │
│ │
│ PERCLOS 阈值: │
│ ├── 正常:< 15% │
│ ├── 轻度疲劳:15-30% │
│ ├── 中度疲劳:30-50% │
│ ├── 重度疲劳:≥ 50% │
│ │
│ 窗口大小: │
│ ├── 30秒:快速响应疲劳变化 │
│ ├── 60秒:平衡响应速度和稳定性 │
│ ├── 5分钟:长期疲劳评估 │
│ │
└─────────────────────────────────────────────────────────────┘

九、总结

要点 说明
EAR 计算 基于眼睛关键点距离比,检测眼睛闭合程度
眨眼检测 基于闭眼时长和间隔,过滤噪声
PERCLOS 统计窗口内的闭眼比例,评估疲劳程度
疲劳等级 0-3 级,基于 PERCLOS 30秒阈值
告警系统 带冷却时间的告警触发机制

系列进度: 43/55
更新时间: 2026-03-12


MediaPipe 系列 43:IMS DMS 架构——眼动追踪流水线完整实现
https://dapalm.com/2026/03/12/MediaPipe系列43-IMS-DMS架构:眼动追踪流水线/
作者
Mars
发布于
2026年3月12日
许可协议