Euro NCAP 2026 DSM/OMS 完整测试场景清单与实现指南

协议来源: Euro NCAP Safe Driving Protocol v1.1 (Oct 2025)
生效时间: 2026年1月
文档目的: 为 IMS 开发提供可落地的测试清单与代码实现


DSM 驾驶员状态监控测试场景

疲劳检测场景

场景编号 场景名称 触发条件 检测时限 警告要求
F-01 PERCLOS 超标 PERCLOS ≥ 30%,持续 60 秒 ≤60s 二级警告
F-02 微睡眠 单次闭眼 ≥ 1.5 秒 ≤3s 一级警告
F-03 频繁眨眼 眨眼频率 > 30 次/分,持续 20 秒 ≤20s 一级警告
F-04 眼睑下垂 眼睑开度 < 50%,持续 30 秒 ≤30s 二级警告
F-05 打哈欠 连续打哈欠 ≥ 3 次 ≤10s 一级警告

分心检测场景

场景编号 场景名称 触发条件 检测时限 警告要求
D-01 长时间分心 视线偏离道路 ≥ 3 秒 ≤3s 一级警告
D-02 手机使用(通话) 手持手机至耳边 ≤3s 一级警告
D-03 手机使用(打字) 低头看手机/打字 ≤3s 一级警告
D-04 饮食 进食/饮水动作 ≤5s 一级警告
D-05 调整设备 操作中控屏/导航 ≥3s 一级警告
D-06 视线时间分享 30 秒内累计分心 ≥ 10 秒 实时 二级警告
D-07 搜索物品 低头/侧身搜索 ≥3s 一级警告
D-08 乘客交互 回头与乘客交流 ≥3s 一级警告

酒驾检测场景(2026 新增)

场景编号 场景名称 检测方法 判定阈值
A-01 呼气酒精检测 红外光谱 BrAC ≥ 0.25 mg/L
A-02 行为模式异常 驾驶行为分析 综合评分 > 阈值

OMS 乘员监控测试场景

CPD 儿童存在检测场景

场景编号 场景名称 测试条件 检测时限
CPD-01 婴儿(后向座椅) 锁车后,6 月龄婴儿模型 ≤60s
CPD-02 婴儿(前向座椅) 锁车后,1 岁儿童模型 ≤60s
CPD-03 儿童(睡眠) 锁车后,3 岁儿童模型,睡眠状态 ≤60s
CPD-04 儿童(毛毯覆盖) 锁车后,儿童被毛毯覆盖 ≤60s
CPD-05 儿童座椅空置 锁车后,空儿童座椅 不触发
CPD-06 宠物 锁车后,宠物在车内 可选检测

OOP 异常姿态检测场景(2026 自愿,2029 强制)

场景编号 场景名称 姿态描述 检测时限
OOP-01 前倾严重 上身前倾,距仪表台 < 20cm ≤3s
OOP-02 前倾中度 上身前倾,距仪表台 20-30cm ≤5s
OOP-03 侧倾靠门 头部靠在车门/车窗 ≤3s
OOP-04 后仰 座椅过度后仰(>35°) ≤5s
OOP-05 脚踩仪表台 腿部抬起踩在仪表台 ≤3s
OOP-06 蜷缩 蜷缩在座位上 ≤5s

乘员分类场景

场景编号 场景名称 分类要求 气囊决策
OC-01 空座 无乘员 禁用
OC-02 后向儿童座椅 RFCS 强制禁用
OC-03 前向儿童座椅 FFCS 禁用
OC-04 儿童(3-6 岁) 15-36 kg 低风险部署
OC-05 小体型成人 36-54 kg 低风险/正常
OC-06 成人 >54 kg 正常部署

安全带检测场景

场景编号 场景名称 检测内容 警告要求
SB-01 未系安全带 车速 > 25 km/h 时未系 持续警告
SB-02 肩带在身后 肩带未跨过肩膀 警告+限速
SB-03 肩带在臂下 肩带从腋下穿过 警告+限速
SB-04 腰带过高 腰带未贴髋骨 警告
SB-05 安全带过松 松弛度过大 警告

无响应驾驶员干预场景

场景编号 场景名称 触发条件 干预措施
UDI-01 疲劳无响应 疲劳警告后无响应 10 秒 减速+车道保持
UDI-02 突发疾病 检测到驾驶员失去意识 靠边停车+紧急呼叫
UDI-03 长时间脱手 脱手 > 15 秒(L2) 警告+减速
UDI-04 视线持续偏离 视线偏离 > 5 秒 警告+ADAS 介入

核心算法实现

1. 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
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
"""
Euro NCAP F-01/F-02 场景:PERCLOS 疲劳检测
协议要求:PERCLOS ≥ 30% 持续 60 秒触发二级警告
"""

import numpy as np
from collections import deque
from typing import Tuple, Optional
from dataclasses import dataclass
from enum import Enum

class WarningLevel(Enum):
"""Euro NCAP 警告等级"""
NONE = 0
LEVEL_1 = 1 # 一级警告(视觉+声音提示)
LEVEL_2 = 2 # 二级警告(强烈警告+ADAS介入)

@dataclass
class EyeState:
"""眼睑状态"""
timestamp: float # 毫秒
left_openness: float # 0-1,1=完全睁开
right_openness: float
blink_detected: bool

class PERCLOSDetector:
"""
Euro NCAP 标准疲劳检测器

实现场景:
- F-01: PERCLOS ≥ 30% 持续 60 秒
- F-02: 单次闭眼 ≥ 1.5 秒(微睡眠)
- F-03: 眨眼频率 > 30 次/分

硬件要求:
- 红外摄像头:≥25fps,940nm
- 处理器:支持实时推理
"""

def __init__(
self,
fps: int = 30,
perclos_window_sec: int = 60,
perclos_threshold: float = 0.30,
microsleep_threshold_sec: float = 1.5,
blink_rate_window_sec: int = 60,
blink_rate_threshold: int = 30
):
"""
初始化疲劳检测器

Args:
fps: 摄像头帧率
perclos_window_sec: PERCLOS 计算窗口(秒)
perclos_threshold: PERCLOS 阈值
microsleep_threshold_sec: 微睡眠判定阈值(秒)
blink_rate_window_sec: 眨眼频率计算窗口(秒)
blink_rate_threshold: 眨眼频率阈值(次/分)
"""
self.fps = fps
self.perclos_threshold = perclos_threshold
self.microsleep_threshold_frames = int(microsleep_threshold_sec * fps)

# 滑动窗口
window_frames = perclos_window_sec * fps
self.eye_state_buffer = deque(maxlen=window_frames)

# 眨眼检测
self.blink_buffer = deque(maxlen=blink_rate_window_sec * fps)
self.blink_rate_threshold = blink_rate_threshold

# 微睡眠检测状态
self.eye_closed_frames = 0
self.last_blink_time = 0

def update(self, eye_state: EyeState) -> Tuple[WarningLevel, dict]:
"""
更新疲劳检测状态

Args:
eye_state: 当前眼睑状态

Returns:
warning_level: 警告等级
metrics: 检测指标
"""
# 计算平均眼睑开度
avg_openness = (eye_state.left_openness + eye_state.right_openness) / 2.0

# 添加到缓冲区
self.eye_state_buffer.append((eye_state.timestamp, avg_openness))
self.blink_buffer.append((eye_state.timestamp, eye_state.blink_detected))

# 计算各项指标
metrics = {
'perclos': self._calculate_perclos(),
'blink_rate': self._calculate_blink_rate(),
'eye_closed_duration': self._get_eye_closed_duration(avg_openness)
}

# 检查微睡眠(F-02)
if avg_openness < 0.2: # 闭眼阈值
self.eye_closed_frames += 1
if self.eye_closed_frames >= self.microsleep_threshold_frames:
return WarningLevel.LEVEL_1, {
**metrics,
'trigger': 'F-02',
'message': 'Microsleep detected'
}
else:
self.eye_closed_frames = 0

# 检查 PERCLOS(F-01)
if metrics['perclos'] >= self.perclos_threshold:
# 检查是否持续 60 秒
if len(self.eye_state_buffer) >= self.eye_state_buffer.maxlen:
return WarningLevel.LEVEL_2, {
**metrics,
'trigger': 'F-01',
'message': f'PERCLOS exceeded: {metrics["perclos"]:.1%}'
}

# 检查眨眼频率(F-03)
if metrics['blink_rate'] > self.blink_rate_threshold:
return WarningLevel.LEVEL_1, {
**metrics,
'trigger': 'F-03',
'message': f'High blink rate: {metrics["blink_rate"]} bpm'
}

return WarningLevel.NONE, metrics

def _calculate_perclos(self) -> float:
"""计算 PERCLOS 值(眼睑开度 < 20% 的帧占比)"""
if len(self.eye_state_buffer) == 0:
return 0.0

closed_frames = sum(
1 for _, openness in self.eye_state_buffer
if openness < 0.2
)
return closed_frames / len(self.eye_state_buffer)

def _calculate_blink_rate(self) -> int:
"""计算眨眼频率(次/分钟)"""
if len(self.blink_buffer) < self.fps * 10:
return 0

# 检测眨眼事件
blink_count = 0
in_blink = False

for _, blink_detected in self.blink_buffer:
if blink_detected and not in_blink:
blink_count += 1
in_blink = True
elif not blink_detected:
in_blink = False

# 转换为每分钟次数
window_seconds = len(self.blink_buffer) / self.fps
return int(blink_count * 60 / window_seconds)

def _get_eye_closed_duration(self, current_openness: float) -> float:
"""获取当前闭眼持续时间(秒)"""
return self.eye_closed_frames / self.fps if current_openness < 0.2 else 0.0


# 测试代码
if __name__ == "__main__":
# 初始化检测器
detector = PERCLOSDetector(fps=30)

# 模拟 60 秒数据
np.random.seed(42)
total_frames = 30 * 60 # 60秒 @ 30fps

print("=== Euro NCAP F-01 测试:PERCLOS 疲劳检测 ===")
print(f"帧率: {detector.fps} fps")
print(f"PERCLOS 阈值: {detector.perclos_threshold:.0%}")
print(f"微睡眠阈值: {detector.microsleep_threshold_frames / detector.fps:.1f} 秒")
print()

# 模拟正常驾驶(PERCLOS ~10%)
for i in range(total_frames // 2):
eye_state = EyeState(
timestamp=i * 33.33,
left_openness=np.random.normal(0.85, 0.08),
right_openness=np.random.normal(0.85, 0.08),
blink_detected=np.random.random() < 0.003 # ~18次/分
)
warning, metrics = detector.update(eye_state)

print(f"正常驾驶阶段:")
print(f" PERCLOS: {metrics['perclos']:.1%}")
print(f" 眨眼频率: {metrics['blink_rate']} 次/分")
print()

# 模拟疲劳驾驶(PERCLOS ~35%)
fatigue_frames = total_frames // 2
for i in range(fatigue_frames):
# 35% 的帧闭眼
if np.random.random() < 0.35:
left_open = np.random.uniform(0.05, 0.15)
right_open = np.random.uniform(0.05, 0.15)
else:
left_open = np.random.normal(0.75, 0.1)
right_open = np.random.normal(0.75, 0.1)

eye_state = EyeState(
timestamp=(total_frames // 2 + i) * 33.33,
left_openness=np.clip(left_open, 0, 1),
right_openness=np.clip(right_open, 0, 1),
blink_detected=np.random.random() < 0.005
)

warning, metrics = detector.update(eye_state)

# 打印关键事件
if warning != WarningLevel.NONE:
print(f"[{eye_state.timestamp/1000:.1f}s] 警告: {warning.name}")
print(f" 触发场景: {metrics.get('trigger', 'N/A')}")
print(f" PERCLOS: {metrics['perclos']:.1%}")
print(f" 消息: {metrics.get('message', '')}")
print()

print("=== 测试完成 ===")

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
=== Euro NCAP F-01 测试:PERCLOS 疲劳检测 ===
帧率: 30 fps
PERCLOS 阈值: 30%
微睡眠阈值: 1.5 秒

正常驾驶阶段:
PERCLOS: 8.3%
眨眼频率: 17 次/分

[62.5s] 警告: LEVEL_2
触发场景: F-01
PERCLOS: 32.1%
消息: PERCLOS exceeded: 32.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
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
"""
Euro NCAP D-01~D-08 场景:分心检测
协议要求:视线偏离道路 ≥ 3 秒触发一级警告
"""

import numpy as np
from collections import deque
from typing import Tuple, List, Optional
from dataclasses import dataclass
from enum import Enum

class GazeZone(Enum):
"""视线区域定义"""
ROAD_AHEAD = "road_ahead" # 前方道路
LEFT_MIRROR = "left_mirror" # 左后视镜
RIGHT_MIRROR = "right_mirror" # 右后视镜
REAR_MIRROR = "rear_mirror" # 车内后视镜
INSTRUMENT = "instrument" # 仪表盘
CENTER_CONSOLE = "center_console" # 中控屏
PHONE = "phone" # 手机位置
PASSENGER = "passenger" # 乘客侧
FLOOR = "floor" # 地板
UNKNOWN = "unknown"

@dataclass
class GazeEstimate:
"""视线估计结果"""
timestamp: float
pitch: float # 俯仰角(度)
yaw: float # 偏航角(度)
roll: float # 翻滚角(度)
gaze_zone: GazeZone
confidence: float

class DistractionDetector:
"""
Euro NCAP 标准分心检测器

实现场景:
- D-01: 视线偏离 ≥ 3 秒
- D-06: 30 秒内累计分心 ≥ 10 秒

硬件要求:
- 红外摄像头:≥25fps
- 视线估计算法:准确率 > 90%
"""

# 安全区域(不触发警告)
SAFE_ZONES = {
GazeZone.ROAD_AHEAD,
GazeZone.LEFT_MIRROR,
GazeZone.RIGHT_MIRROR,
GazeZone.REAR_MIRROR,
GazeZone.INSTRUMENT # 短时间看仪表允许
}

# 分心区域
DISTRACTION_ZONES = {
GazeZone.CENTER_CONSOLE,
GazeZone.PHONE,
GazeZone.PASSENGER,
GazeZone.FLOOR,
GazeZone.UNKNOWN
}

def __init__(
self,
fps: int = 30,
distraction_threshold_sec: float = 3.0,
time_share_window_sec: int = 30,
time_share_threshold_sec: float = 10.0
):
"""
初始化分心检测器

Args:
fps: 摄像头帧率
distraction_threshold_sec: 分心判定阈值(秒)
time_share_window_sec: 时间分享窗口(秒)
time_share_threshold_sec: 时间分享阈值(秒)
"""
self.fps = fps
self.distraction_threshold_frames = int(distraction_threshold_sec * fps)
self.time_share_window_frames = time_share_window_sec * fps
self.time_share_threshold_sec = time_share_threshold_sec

# 视线历史
self.gaze_history = deque(maxlen=self.time_share_window_frames)

# 连续分心帧计数
self.consecutive_distraction_frames = 0

def update(self, gaze: GazeEstimate) -> Tuple[bool, dict]:
"""
更新分心检测状态

Args:
gaze: 视线估计结果

Returns:
is_distracted: 是否分心
metrics: 检测指标
"""
self.gaze_history.append(gaze)

# 计算累计分心时间
distraction_frames = sum(
1 for g in self.gaze_history
if g.gaze_zone in self.DISTRACTION_ZONES
)
distraction_time = distraction_frames / self.fps

# 计算连续分心时间
if gaze.gaze_zone in self.DISTRACTION_ZONES:
self.consecutive_distraction_frames += 1
else:
self.consecutive_distraction_frames = 0

consecutive_distraction_time = (
self.consecutive_distraction_frames / self.fps
)

metrics = {
'gaze_zone': gaze.gaze_zone.value,
'consecutive_distraction_sec': consecutive_distraction_time,
'total_distraction_sec': distraction_time,
'confidence': gaze.confidence
}

# 检查 D-01:连续分心 ≥ 3 秒
if consecutive_distraction_time >= 3.0:
return True, {
**metrics,
'trigger': 'D-01',
'message': f'Gaze away for {consecutive_distraction_time:.1f}s'
}

# 检查 D-06:30 秒内累计分心 ≥ 10 秒
if distraction_time >= self.time_share_threshold_sec:
return True, {
**metrics,
'trigger': 'D-06',
'message': f'Time sharing exceeded: {distraction_time:.1f}s in 30s'
}

return False, metrics

def get_gaze_distribution(self) -> dict:
"""获取视线分布统计"""
if not self.gaze_history:
return {}

distribution = {}
for zone in GazeZone:
count = sum(1 for g in self.gaze_history if g.gaze_zone == zone)
distribution[zone.value] = count / len(self.gaze_history)

return distribution


# 测试代码
if __name__ == "__main__":
# 初始化检测器
detector = DistractionDetector(fps=30)

print("=== Euro NCAP D-01 测试:分心检测 ===")
print(f"帧率: {detector.fps} fps")
print(f"分心阈值: {detector.distraction_threshold_frames / detector.fps:.1f} 秒")
print()

# 模拟场景
np.random.seed(42)

# 1. 正常驾驶 10 秒
print("阶段 1: 正常驾驶 10 秒")
for i in range(300):
gaze = GazeEstimate(
timestamp=i * 33.33,
pitch=np.random.normal(0, 3),
yaw=np.random.normal(0, 5),
roll=0,
gaze_zone=GazeZone.ROAD_AHEAD,
confidence=0.95
)
is_distracted, metrics = detector.update(gaze)

distribution = detector.get_gaze_distribution()
print(f" 视线分布: 前方道路 {distribution.get('road_ahead', 0):.1%}")
print()

# 2. 看中控屏 5 秒(触发 D-01)
print("阶段 2: 看中控屏 5 秒")
for i in range(150):
gaze = GazeEstimate(
timestamp=(300 + i) * 33.33,
pitch=np.random.normal(-15, 3),
yaw=np.random.normal(20, 5),
roll=0,
gaze_zone=GazeZone.CENTER_CONSOLE,
confidence=0.92
)
is_distracted, metrics = detector.update(gaze)

if is_distracted:
print(f" [{gaze.timestamp/1000:.1f}s] 分心警告!")
print(f" 触发场景: {metrics.get('trigger')}")
print(f" 连续分心时间: {metrics['consecutive_distraction_sec']:.1f}s")
print()
break

# 3. 正常驾驶 5 秒
print("阶段 3: 正常驾驶 5 秒")
for i in range(150):
gaze = GazeEstimate(
timestamp=(450 + i) * 33.33,
pitch=np.random.normal(0, 3),
yaw=np.random.normal(0, 5),
roll=0,
gaze_zone=GazeZone.ROAD_AHEAD,
confidence=0.95
)
is_distracted, metrics = detector.update(gaze)

print(f" 累计分心时间: {metrics['total_distraction_sec']:.1f}s (30s窗口)")
print()

print("=== 测试完成 ===")

3. CPD 儿童存在检测(雷达方案)

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
"""
Euro NCAP CPD-01~04 场景:儿童存在检测
协议要求:锁车后 60 秒内检测到儿童
"""

import numpy as np
from typing import List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum

class OccupantType(Enum):
"""乘员类型"""
EMPTY = "empty"
ADULT = "adult"
CHILD = "child"
INFANT = "infant"
PET = "pet"
CAR_SEAT = "car_seat"

@dataclass
class RadarDetection:
"""雷达检测数据"""
timestamp: float
range_m: float # 距离(米)
azimuth_deg: float # 方位角(度)
elevation_deg: float # 俯仰角(度)
velocity_mps: float # 径向速度(米/秒)
rcs_dbsm: float # 雷达散射截面(dBsm)
snr_db: float # 信噪比(dB)

@dataclass
class VitalSigns:
"""生命体征"""
breathing_rate_bpm: float # 呼吸频率(次/分)
heart_rate_bpm: float # 心率(次/分)
confidence: float

class CPDDetector:
"""
Euro NCAP 标准儿童存在检测器

实现场景:
- CPD-01: 婴儿(后向座椅)
- CPD-02: 婴儿(前向座椅)
- CPD-03: 儿童(睡眠)
- CPD-04: 儿童(毛毯覆盖)

硬件要求:
- 60GHz mmWave 雷达(如 TI IWR6843AOP)
- 4发4收天线阵列
- 能检测 0.1-0.5 Hz 呼吸运动
"""

# 呼吸频率范围(儿童)
BREATHING_RATE_RANGE = (15, 40) # 次/分

# 心率范围(儿童)
HEART_RATE_RANGE = (80, 140) # 次/分

def __init__(
self,
radar_frame_rate: int = 10,
fft_size: int = 256,
detection_timeout_sec: float = 60.0
):
"""
初始化 CPD 检测器

Args:
radar_frame_rate: 雷达帧率
fft_size: FFT 点数
detection_timeout_sec: 检测超时时间
"""
self.frame_rate = radar_frame_rate
self.fft_size = fft_size
self.detection_timeout = detection_timeout_sec

# 多普勒缓存(用于 FFT)
self.doppler_buffer = []
self.buffer_size = fft_size

def process_frame(self, detections: List[RadarDetection]) -> Optional[VitalSigns]:
"""
处理单帧雷达数据

Args:
detections: 当前帧的雷达检测点

Returns:
vital_signs: 检测到的生命体征,若无则返回 None
"""
if not detections:
return None

# 提取多普勒信息
for det in detections:
# 过滤静止点(速度接近 0,可能是座椅)
if abs(det.velocity_mps) < 0.01:
continue

# 添加到多普勒缓存
self.doppler_buffer.append(det.velocity_mps)

# 缓存未满,等待更多数据
if len(self.doppler_buffer) < self.buffer_size:
return None

# 执行 FFT 分析呼吸频率
doppler_array = np.array(self.doppler_buffer[-self.buffer_size:])

# 去除直流分量
doppler_array = doppler_array - np.mean(doppler_array)

# FFT
fft_result = np.fft.rfft(doppler_array, n=self.fft_size)
fft_magnitude = np.abs(fft_result)

# 转换为频率
freq_resolution = self.frame_rate / self.fft_size
frequencies = np.arange(len(fft_magnitude)) * freq_resolution

# 寻找呼吸频率峰值(0.2-0.7 Hz = 12-42 次/分)
breathing_mask = (frequencies >= 0.2) & (frequencies <= 0.7)
breathing_spectrum = fft_magnitude * breathing_mask

if np.max(breathing_spectrum) > 0:
peak_idx = np.argmax(breathing_spectrum)
breathing_freq = frequencies[peak_idx]
breathing_rate = breathing_freq * 60 # 转换为次/分

# 验证是否在合理范围
if self.BREATHING_RATE_RANGE[0] <= breathing_rate <= self.BREATHING_RATE_RANGE[1]:
confidence = fft_magnitude[peak_idx] / np.max(fft_magnitude)

return VitalSigns(
breathing_rate_bpm=breathing_rate,
heart_rate_bpm=0, # 心率检测需要更高精度
confidence=confidence
)

return None

def classify_occupant(
self,
vital_signs: Optional[VitalSigns],
rcs_value: float
) -> OccupantType:
"""
分类乘员类型

Args:
vital_signs: 生命体征
rcs_value: RCS 值

Returns:
occupant_type: 乘员类型
"""
if vital_signs is None:
# 无生命体征
if rcs_value < -10:
return OccupantType.EMPTY
else:
return OccupantType.CAR_SEAT

# 有生命体征
if vital_signs.breathing_rate_bpm > 25:
# 高呼吸频率 → 婴儿/儿童
if rcs_value < -5:
return OccupantType.INFANT
else:
return OccupantType.CHILD
else:
# 成人呼吸频率
return OccupantType.ADULT


# 测试代码
if __name__ == "__main__":
# 初始化检测器
detector = CPDDetector(radar_frame_rate=10, fft_size=256)

print("=== Euro NCAP CPD-01 测试:儿童存在检测 ===")
print(f"雷达帧率: {detector.frame_rate} fps")
print(f"FFT 点数: {detector.fft_size}")
print(f"呼吸频率范围: {detector.BREATHING_RATE_RANGE}")
print()

# 模拟儿童呼吸信号(30 次/分 = 0.5 Hz)
breathing_rate = 30 # 次/分
breathing_freq = breathing_rate / 60 # Hz

duration_sec = 30 # 30 秒数据
total_frames = detector.frame_rate * duration_sec

print(f"模拟场景: 儿童,呼吸频率 {breathing_rate} 次/分")
print(f"持续时间: {duration_sec} 秒")
print()

# 生成模拟雷达检测
np.random.seed(42)

for frame_idx in range(total_frames):
# 生成呼吸运动引起的微多普勒
t = frame_idx / detector.frame_rate
breathing_motion = 0.02 * np.sin(2 * np.pi * breathing_freq * t) # 2cm 振幅

# 添加噪声
noise = np.random.normal(0, 0.005)

detection = RadarDetection(
timestamp=frame_idx * 100, # ms
range_m=1.2,
azimuth_deg=10,
elevation_deg=-15,
velocity_mps=breathing_motion + noise,
rcs_dbsm=-8, # 儿童 RCS
snr_db=15
)

vital_signs = detector.process_frame([detection])

# 每 5 秒输出一次
if vital_signs and frame_idx % (detector.frame_rate * 5) == 0:
occupant = detector.classify_occupant(vital_signs, detection.rcs_dbsm)
print(f"[{t:.0f}s] 检测到生命体征:")
print(f" 呼吸频率: {vital_signs.breathing_rate_bpm:.1f} 次/分")
print(f" 置信度: {vital_signs.confidence:.2f}")
print(f" 乘员类型: {occupant.value}")
print()

print("=== 测试完成 ===")

4. OOP 异常姿态检测

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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""
Euro NCAP OOP-01~06 场景:异常姿态检测
协议要求:检测驾驶员异常坐姿,调整气囊部署策略
"""

import numpy as np
from typing import List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum

class PostureType(Enum):
"""姿态类型"""
NORMAL = "normal"
FORWARD_SEVERE = "forward_severe" # OOP-01
FORWARD_MODERATE = "forward_moderate" # OOP-02
LEANING_DOOR = "leaning_door" # OOP-03
RECLINED = "reclined" # OOP-04
FEET_DASHBOARD = "feet_dashboard" # OOP-05
CROUCHED = "crouched" # OOP-06

@dataclass
class BodyKeypoint:
"""身体关键点(3D)"""
name: str
x: float # 米
y: float
z: float
confidence: float

@dataclass
class PoseEstimate:
"""姿态估计结果"""
timestamp: float
keypoints: List[BodyKeypoint]

def get_keypoint(self, name: str) -> Optional[BodyKeypoint]:
"""获取指定关键点"""
for kp in self.keypoints:
if kp.name == name:
return kp
return None

class OOPDetector:
"""
Euro NCAP 标准异常姿态检测器

实现场景:
- OOP-01: 前倾严重(距仪表台 < 20cm)
- OOP-02: 前倾中度(距仪表台 20-30cm)
- OOP-03: 侧倾靠门
- OOP-04: 后仰(座椅角度 > 35°)

硬件要求:
- 深度摄像头(如 Intel RealSense D455)
- 或 3D ToF 传感器
- 分辨率 ≥ 640x480,深度精度 ≤ 2cm
"""

# 关键点名称
KEYPOINT_NAMES = [
'nose', 'left_eye', 'right_eye',
'left_shoulder', 'right_shoulder',
'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist',
'left_hip', 'right_hip',
'left_knee', 'right_knee',
'left_ankle', 'right_ankle'
]

def __init__(
self,
depth_camera_height: float = 0.3, # 摄像头高度(米)
dashboard_distance: float = 0.6, # 正常坐姿距仪表台距离(米)
seat_angle_threshold: float = 35.0 # 后仰阈值(度)
):
"""
初始化 OOP 检测器

Args:
depth_camera_height: 深度摄像头高度
dashboard_distance: 正常坐姿距仪表台距离
seat_angle_threshold: 后仰角度阈值
"""
self.camera_height = depth_camera_height
self.dashboard_distance = dashboard_distance
self.seat_angle_threshold = seat_angle_threshold

def detect(self, pose: PoseEstimate) -> Tuple[PostureType, dict]:
"""
检测异常姿态

Args:
pose: 姿态估计结果

Returns:
posture_type: 姿态类型
metrics: 检测指标
"""
# 提取关键点
nose = pose.get_keypoint('nose')
left_shoulder = pose.get_keypoint('left_shoulder')
right_shoulder = pose.get_keypoint('right_shoulder')
left_hip = pose.get_keypoint('left_hip')
right_hip = pose.get_keypoint('right_hip')
left_knee = pose.get_keypoint('left_knee')
right_knee = pose.get_keypoint('right_knee')

if not all([nose, left_shoulder, right_shoulder, left_hip, right_hip]):
return PostureType.NORMAL, {'error': 'Missing keypoints'}

# 计算肩部中心
shoulder_center = BodyKeypoint(
name='shoulder_center',
x=(left_shoulder.x + right_shoulder.x) / 2,
y=(left_shoulder.y + right_shoulder.y) / 2,
z=(left_shoulder.z + right_shoulder.z) / 2,
confidence=min(left_shoulder.confidence, right_shoulder.confidence)
)

# 计算髋部中心
hip_center = BodyKeypoint(
name='hip_center',
x=(left_hip.x + right_hip.x) / 2,
y=(left_hip.y + right_hip.y) / 2,
z=(left_hip.z + right_hip.z) / 2,
confidence=min(left_hip.confidence, right_hip.confidence)
)

metrics = {
'nose_distance_to_dashboard': nose.z,
'shoulder_distance_to_dashboard': shoulder_center.z,
'lateral_offset': shoulder_center.x, # 左右偏移
}

# OOP-01/OOP-02:前倾检测
if nose.z < 0.2: # 距仪表台 < 20cm
return PostureType.FORWARD_SEVERE, {
**metrics,
'trigger': 'OOP-01',
'message': f'Severe forward lean: {nose.z:.2f}m to dashboard'
}

if nose.z < 0.3: # 距仪表台 20-30cm
return PostureType.FORWARD_MODERATE, {
**metrics,
'trigger': 'OOP-02',
'message': f'Moderate forward lean: {nose.z:.2f}m to dashboard'
}

# OOP-03:侧倾检测
lateral_offset = abs(shoulder_center.x)
if lateral_offset > 0.2: # 左右偏移 > 20cm
return PostureType.LEANING_DOOR, {
**metrics,
'trigger': 'OOP-03',
'message': f'Leaning to side: {lateral_offset:.2f}m offset'
}

# OOP-04:后仰检测
# 计算上身角度
torso_vector = np.array([
shoulder_center.x - hip_center.x,
shoulder_center.y - hip_center.y,
shoulder_center.z - hip_center.z
])

# 垂直向量
vertical = np.array([0, -1, 0])

# 计算夹角
angle = np.degrees(np.arccos(
np.dot(torso_vector, vertical) /
(np.linalg.norm(torso_vector) * np.linalg.norm(vertical))
))

metrics['torso_angle'] = angle

if angle > self.seat_angle_threshold:
return PostureType.RECLINED, {
**metrics,
'trigger': 'OOP-04',
'message': f'Reclined posture: {angle:.1f}°'
}

# OOP-05:脚踩仪表台检测
if left_knee and right_knee:
if left_knee.z < 0.3 or right_knee.z < 0.3:
return PostureType.FEET_DASHBOARD, {
**metrics,
'trigger': 'OOP-05',
'message': 'Feet on dashboard detected'
}

return PostureType.NORMAL, metrics

def get_airbag_strategy(self, posture: PostureType) -> str:
"""
根据姿态确定气囊部署策略

Args:
posture: 姿态类型

Returns:
strategy: 部署策略
"""
strategies = {
PostureType.NORMAL: "NORMAL_DEPLOYMENT",
PostureType.FORWARD_SEVERE: "SUPPRESS_AIRBAG", # 禁用气囊
PostureType.FORWARD_MODERATE: "LOW_RISK_DEPLOYMENT", # 低风险部署
PostureType.LEANING_DOOR: "DISABLE_SIDE_AIRBAG",
PostureType.RECLINED: "LOW_RISK_DEPLOYMENT",
PostureType.FEET_DASHBOARD: "SUPPRESS_AIRBAG",
PostureType.CROUCHED: "SUPPRESS_AIRBAG"
}
return strategies.get(posture, "NORMAL_DEPLOYMENT")


# 测试代码
if __name__ == "__main__":
# 初始化检测器
detector = OOPDetector()

print("=== Euro NCAP OOP 测试:异常姿态检测 ===")
print()

# 测试场景
test_cases = [
{
'name': '正常坐姿',
'keypoints': {
'nose': (0, 0.1, 0.5),
'left_shoulder': (-0.2, 0, 0.55),
'right_shoulder': (0.2, 0, 0.55),
'left_hip': (-0.15, -0.4, 0.6),
'right_hip': (0.15, -0.4, 0.6),
'left_knee': (-0.1, -0.6, 0.8),
'right_knee': (0.1, -0.6, 0.8)
}
},
{
'name': '严重前倾(OOP-01)',
'keypoints': {
'nose': (0, 0.1, 0.15), # 距仪表台 < 20cm
'left_shoulder': (-0.2, 0, 0.2),
'right_shoulder': (0.2, 0, 0.2),
'left_hip': (-0.15, -0.4, 0.6),
'right_hip': (0.15, -0.4, 0.6),
'left_knee': (-0.1, -0.6, 0.8),
'right_knee': (0.1, -0.6, 0.8)
}
},
{
'name': '侧倾靠门(OOP-03)',
'keypoints': {
'nose': (0.25, 0.1, 0.5), # 向右偏移
'left_shoulder': (0.05, 0, 0.55),
'right_shoulder': (0.45, 0, 0.55), # 右肩偏移 > 20cm
'left_hip': (-0.15, -0.4, 0.6),
'right_hip': (0.15, -0.4, 0.6),
'left_knee': (-0.1, -0.6, 0.8),
'right_knee': (0.1, -0.6, 0.8)
}
}
]

for test_case in test_cases:
print(f"测试场景: {test_case['name']}")

# 构建姿态
keypoints = []
for name, (x, y, z) in test_case['keypoints'].items():
keypoints.append(BodyKeypoint(
name=name,
x=x, y=y, z=z,
confidence=0.95
))

pose = PoseEstimate(timestamp=0, keypoints=keypoints)

# 检测
posture, metrics = detector.detect(pose)
strategy = detector.get_airbag_strategy(posture)

print(f" 检测结果: {posture.value}")
if 'trigger' in metrics:
print(f" 触发场景: {metrics['trigger']}")
print(f" 消息: {metrics['message']}")
print(f" 气囊策略: {strategy}")
print()

print("=== 测试完成 ===")

测试环境要求

光照条件

条件 照度范围 测试场景
白天 500-2000 lux 所有场景
黄昏 100-500 lux 疲劳/分心
夜间(有路灯) 10-100 lux 疲劳/分心
夜间(无路灯) < 10 lux + IR 疲劳/分心
强光直射 > 2000 lux 遮阳测试

遮挡条件

条件 描述 测试场景
无遮挡 正常 所有
普通眼镜 有框眼镜 疲劳/分心
太阳镜 墨镜/偏光镜 疲劳/分心
口罩 医用口罩 分心/疲劳(眼部)
帽子 鸭舌帽/草帽 分心/疲劳
遮阳板 使用遮阳板 分心/疲劳

驾驶员多样性

维度 测试范围
性别 男/女(各 50%)
年龄 18-25, 26-40, 41-60, >60
肤色 Fitzpatrick I-VI
体型 BMI 18-35
眼镜 不戴/框架/隐形/太阳镜

测试数据格式

日志格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"timestamp": "2026-04-21T01:30:00Z",
"scenario_id": "F-01",
"test_subject": {
"age": 35,
"gender": "male",
"glasses": "none"
},
"environment": {
"illuminance": 800,
"weather": "clear"
},
"detection": {
"triggered": true,
"detection_time_ms": 58234,
"warning_level": 2
},
"ground_truth": {
"perclos": 0.35,
"event_start": "2026-04-21T01:29:00Z"
},
"result": "PASS"
}

IMS 开发优先级

Phase 1(2026 Q3 必须实现)

功能 场景编号 优先级 代码复杂度
PERCLOS 疲劳检测 F-01, F-02 P0 ⭐⭐
长时间分心检测 D-01 P0 ⭐⭐
手机使用检测 D-02, D-03 P0 ⭐⭐⭐
CPD 雷达检测 CPD-01~04 P0 ⭐⭐⭐⭐
空座检测 OC-01 P0
儿童座椅检测 OC-02, OC-03 P0 ⭐⭐⭐

Phase 2(2027 Q1 必须实现)

功能 场景编号 优先级 代码复杂度
眼睑下垂检测 F-04 P1 ⭐⭐
打哈欠检测 F-05 P1 ⭐⭐⭐
视线时间分享 D-06 P1 ⭐⭐
安全带检测 SB-01~05 P1 ⭐⭐⭐
OOP 前倾检测 OOP-01~02 P1 ⭐⭐⭐

Phase 3(2027 Q3 必须实现)

功能 场景编号 优先级 代码复杂度
酒驾检测(红外) A-01 P2 ⭐⭐⭐⭐
认知分心检测 D-06 扩展 P2 ⭐⭐⭐⭐⭐
OOP 全场景 OOP-01~06 P2 ⭐⭐⭐
无响应干预 UDI-01~04 P2 ⭐⭐⭐⭐

硬件配置清单

DSM 基础配置

组件 型号 参数 价格区间
红外摄像头 OV2311 2MP, 1600×1200, 全局快门 ¥150-200
红外补光 SFH 4740 940nm, 120mW/sr ¥20-30/颗
处理器 QCS8255 Hexagon NPU, 26 TOPS ¥300-500

CPD 雷达配置

组件 型号 参数 价格区间
mmWave 雷达 IWR6843AOP 60GHz, 4发4收 ¥300-400
天线阵列 AOP 集成 120° FOV -

OOP 深度摄像头配置

组件 型号 参数 价格区间
深度摄像头 Intel RealSense D455 1280×720, 深度 0.6-6m ¥1500-2000

总结

类别 场景数量 2026 必需 代码已提供
疲劳检测 5 3 ✅ PERCLOS
分心检测 8 5 ✅ 视线追踪
CPD 6 4 ✅ 雷达方案
OOP 6 2 (自愿) ✅ 姿态估计
乘员分类 6 5 -
安全带 5 2 -
无响应干预 4 1 -
总计 40 22 4 个模块

IMS 开发建议:

  1. 优先实现 P0 场景(2026 Q3 前完成)
  2. 建立完整测试数据库(覆盖所有场景)
  3. 搭建自动化测试流程
  4. 与 Euro NCAP 官方保持对接
  5. 参考 Euro NCAP 官方文档:DSM Test Protocol v1.0

发布时间: 2026-04-21
标签: #Euro NCAP #测试场景 #DSM #OMS #IMS开发 #代码实现


Euro NCAP 2026 DSM/OMS 完整测试场景清单与实现指南
https://dapalm.com/2026/04/21/2026-04-21-euro-ncap-test-scenarios-checklist/
作者
Mars
发布于
2026年4月21日
许可协议