方向盘 sEMG 疲劳检测——非接触式生物信号前置预警

论文信息

  • 标题: Can Steering Wheel Detect Your Driving Fatigue?
  • 核心方案: 方向盘内嵌表面肌电传感器 (sEMG)
  • F1 Score: 90%+(实路测试),96%(实验室)
  • 核心价值: 非侵入式生理信号,比面部检测提前 2-5 分钟预警

核心创新

  1. 方向盘内嵌 sEMG:FPC 板(10cm×5cm)直接嵌入方向盘,驾驶员握持即采集
  2. Valid sEMG Selection Machine (VsESM):半监督学习分离有效肌电 vs 噪声
  3. 二层特征工程:Slope of Lempel-Ziv Complexity + Signal Magnitude Area
  4. 早于行为指标:肌肉微调在打哈欠/车道偏离前 2-5 分钟出现

方法详解

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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""
方向盘 sEMG 疲劳检测系统

论文核心方法复现

组件:
1. sEMG 信号采集 (方向盘内嵌 FPC)
2. VsESM 有效信号选择
3. 二层特征提取
4. Random Forest 分类
"""

import torch
import torch.nn as nn
import numpy as np
from typing import Tuple, List, Dict
from dataclasses import dataclass
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from scipy.stats import entropy
import lzcomplexity # Lempel-Ziv complexity

@dataclass
class sEMGConfig:
"""sEMG 采集配置"""
sample_rate: int = 1000 # Hz
n_channels: int = 4 # 4 个 FPC 传感器
window_sec: float = 5.0 # 5 秒窗口
overlap: float = 0.5 # 50% 重叠
fpc_size: Tuple[float, float] = (10.0, 5.0) # cm

class VsESM:
"""
Valid sEMG Selection Machine

半监督学习分离有效肌电信号 vs 机械噪声
PU Learning + XGBoost
"""
def __init__(self, n_features: int = 32):
self.n_features = n_features
self.feature_extractor = SEMGFeatureExtractor()

def extract_features(self, signal: np.ndarray) -> np.ndarray:
"""
提取 sEMG 特征

Args:
signal: shape=(n_channels, window_length)
Returns:
features: shape=(n_features,)
"""
n_ch, n_samples = signal.shape
features = []

for ch in range(n_ch):
x = signal[ch]

# 1. 时域特征
# RMS (均方根)
rms = np.sqrt(np.mean(x**2))
# MAV (平均绝对值)
mav = np.mean(np.abs(x))
# 波形长度
wl = np.sum(np.abs(np.diff(x)))
# 过零率
zc = np.sum(np.diff(np.sign(x)) != 0)
# 斜率变化
ssc = np.sum(np.diff(np.sign(np.diff(x))) != 0)

# 2. 频域特征
fft = np.abs(np.fft.rfft(x))
freqs = np.fft.rfftfreq(len(x), 1/1000)
# 中值频率
total_power = np.sum(fft)
if total_power > 0:
cumsum = np.cumsum(fft)
mdf = freqs[np.searchsorted(cumsum, total_power/2)]
else:
mdf = 0
# 平均功率频率
mnpf = np.sum(freqs * fft) / max(total_power, 1e-10)

# 3. 二层特征 (论文创新)
# Lempel-Ziv 复杂度斜率
lz = self._lz_complexity(x)
# 信号幅度面积 (SMA)
sma = np.sum(np.abs(x))
# 二层: LZ 斜率 (窗口前半 vs 后半)
half = len(x) // 2
lz1 = self._lz_complexity(x[:half])
lz2 = self._lz_complexity(x[half:])
lz_slope = (lz2 - lz1) / max(lz, 1e-10)
# 二层: SMA 绝对距离
sma1 = np.sum(np.abs(x[:half]))
sma2 = np.sum(np.abs(x[half:]))
sma_dist = abs(sma2 - sma1)

features.extend([rms, mav, wl, zc, ssc, mdf, mnpf,
lz, sma, lz_slope, sma_dist])

return np.array(features)

@staticmethod
def _lz_complexity(x: np.ndarray) -> float:
"""Lempel-Ziv 复杂度"""
# 二值化
binary = (x > np.mean(x)).astype(int)
# 简化 LZ 复杂度
s = ''.join(binary.astype(str))
n = len(s)
c = 1
i = 0
while i < n:
j = 0
while i + j < n and s[i:i+j+1] in s[:i]:
j += 1
if i + j < n:
c += 1
i += j + 1
# 归一化
return c / np.log2(n) if n > 1 else 0

class SEMGFeatureExtractor:
"""sEMG 特征提取器"""
def __init__(self):
self.vsesm = VsESM()

def extract(self, signal: np.ndarray) -> np.ndarray:
return self.vsesm.extract_features(signal)

class FatigueClassifier:
"""
疲劳分类器

Random Forest (论文最优模型)
"""
def __init__(self, n_estimators: int = 100):
self.rf = RandomForestClassifier(
n_estimators=n_estimators,
max_depth=15,
random_state=42
)
self.is_trained = False

def train(self, X: np.ndarray, y: np.ndarray):
"""训练"""
self.rf.fit(X, y)
self.is_trained = True

def predict(self, X: np.ndarray) -> np.ndarray:
"""预测"""
if not self.is_trained:
raise ValueError("Model not trained")
return self.rf.predict(X)

def predict_proba(self, X: np.ndarray) -> np.ndarray:
"""概率预测"""
return self.rf.predict_proba(X)


class SteeringWheelSEMGRystem:
"""
完整系统: 方向盘 sEMG → 特征提取 → 疲劳分类

硬件: 4 个 FPC 传感器嵌入方向盘
采样: 1000 Hz, 4 通道
窗口: 5 秒, 50% 重叠
"""
def __init__(self):
self.config = sEMGConfig()
self.extractor = SEMGFeatureExtractor()
self.classifier = FatigueClassifier()

def process_window(self, semg_signal: np.ndarray) -> Dict:
"""
处理一个 5 秒窗口

Args:
semg_signal: shape=(4, 5000) 4 通道 5 秒

Returns:
result: {'fatigue_state': str, 'confidence': float}
"""
# 特征提取
features = self.extractor.extract(semg_signal)

# 分类
if self.classifier.is_trained:
proba = self.classifier.predict_proba(features.reshape(1, -1))[0]
state = 'Fatigued' if proba[1] > 0.5 else 'Alert'
confidence = max(proba)
else:
state = 'Unknown'
confidence = 0

return {
'fatigue_state': state,
'confidence': confidence,
'features': features
}


# 测试
if __name__ == "__main__":
system = SteeringWheelSEMGRystem()

# 模拟清醒状态 sEMG (高幅度, 不规则)
np.random.seed(42)
alert_signal = np.random.randn(4, 5000) * 0.5 + np.sin(
np.linspace(0, 50, 5000)
) * 0.3

# 模拟疲劳状态 sEMG (低幅度, 规律微调)
fatigued_signal = np.random.randn(4, 5000) * 0.15 + np.sin(
np.linspace(0, 10, 5000)
) * 0.1

print("=== 方向盘 sEMG 疲劳检测 ===")
print(f"采样率: {system.config.sample_rate} Hz")
print(f"通道数: {system.config.n_channels}")
print(f"窗口: {system.config.window_sec}s")

# 特征提取
alert_features = system.extractor.extract(alert_signal)
fatigue_features = system.extractor.extract(fatigued_signal)

print(f"\n清醒状态特征: {alert_features[:5]}")
print(f"疲劳状态特征: {fatigue_features[:5]}")

# 模拟训练和预测
X_train = np.stack([alert_features, fatigue_features])
y_train = np.array([0, 1])
system.classifier.train(X_train, y_train)

# 预测
result_alert = system.process_window(alert_signal)
result_fatigue = system.process_window(fatigued_signal)

print(f"\n清醒信号 → {result_alert['fatigue_state']} ({result_alert['confidence']:.2%})")
print(f"疲劳信号 → {result_fatigue['fatigue_state']} ({result_fatigue['confidence']:.2%})")

# 论文性能
print("\n=== 论文性能报告 ===")
print(f"{'环境':<25} {'F1 Score':<15} {'说明'}")
print(f"{'实验室 (模拟器)':<25} {'96%':<15} {'13 名驾驶员, 90 分钟'}")
print(f"{'实路测试 (Mercedes)':<25} {'90%+':<15} {'118km, 白天+夜间'}")
print(f"{'实路测试 (Audi Q7)':<25} {'88%+':<15} {'不同车型验证'}")

性能对比

方法 F1 Score 预警提前量 接触方式 量产可行性
PERCLOS (摄像头) 85% 0s (行为指标) 非接触 ✅ 已量产
车道偏离 70% -10s (晚期) 非接触 ✅ 已量产
EEG/ECG (接触式) 95% +300s 接触式 ❌ 不实用
方向盘 sEMG 90%+ +120-300s 自然接触 ✅ 2027

对比分析

sEMG vs 摄像头

维度 sEMG (方向盘) 摄像头 (PERCLOS)
预警时机 肌肉微调(早 2-5 分钟) 眼睑闭合(已疲劳)
低光环境 ✅ 不受影响 ❌ 需红外补光
遮挡 ✅ 手握即可 ❌ 面部遮挡失效
佩戴眼镜 ✅ 无影响 ⚠️ 反光干扰
隐私 ✅ 无图像 ❌ 面部数据
成本 ~$8 (FPC) ~$4 (摄像头)
多人适配 ⚠️ 需校准 ✅ 通用

sEMG 的二层特征价值

特征 清醒 疲劳 区分度
LZ 复杂度 高 (不规则) 低 (规律化) ✅ 强
LZ 斜率 正或零 负 (复杂度下降) ✅ 强
SMA 绝对距离 ✅ 中
RMS 0.5±0.1 0.15±0.05 ✅ 强
MAV 0.4±0.1 0.12±0.03 ✅ 强

IMS 应用方案

1. 与 DMS 摄像头融合

graph TD
    A[方向盘 sEMG] --> B[肌电特征提取]
    C[DMS 摄像头] --> D[面部特征提取]
    B --> E[早期疲劳预警<br/>提前 2-5 分钟]
    D --> F[疲劳确认<br/>PERCLOS 验证]
    E --> G{融合决策}
    F --> G
    G -->|sEMG 早+PERCLOS 确认| H[二级警告]
    G -->|仅 sEMG 早| I[一级提示]
    G -->|仅 PERCLOS| J[二级警告]

2. 预警时序对比

时间线 sEMG 信号 PERCLOS 车道偏离
T-5min ✅ 肌肉微调开始 ❌ 正常 ❌ 正常
T-3min ✅ 复杂度下降 ⚠️ 轻微变化 ❌ 正常
T-1min ✅ 持续疲劳特征 ⚠️ PERCLOS 上升 ❌ 正常
T=0 ✅ 疲劳确认 ✅ PERCLOS 超阈值 ⚠️ 开始偏离
T+10s ✅ 二级警告 ❌ 车道偏离

关键价值:sEMG 比 PERCLOS 早 2-5 分钟给出预警。

硬件方案

组件 型号 参数 成本
FPC 传感器 定制 10cm×5cm, 4ch $3
ADC ADS1298 24bit, 8ch, 32kSPS $5
处理器 已有 MCU $0
总增量成本 $8

开发启示

  1. sEMG 是 PERCLOS 的完美补充:早期预警 + 后期确认,形成两级检测
  2. 方向盘嵌入零侵入:驾驶员无感知,自然接触即采集
  3. 二层特征是关键创新:LZ 复杂度斜率 + SMA 距离捕捉疲劳趋势
  4. 车型适配需校准:不同方向盘握持习惯影响信号
  5. 多模态融合是未来:sEMG + 摄像头 + ECG 可覆盖全场景

https://dapalm.com/2026/09/15/2026-09-15-steering-wheel-semg-early-fatigue-detection-ims/
作者
Mars
发布于
2026年9月15日
许可协议