Euro NCAP 2026 DSM测试场景详解:完整清单与检测要求
法规来源
官方文档: Euro NCAP Assessment Protocol - Safe Driving - Occupant Monitoring (v1.0, March 2025)
下载链接: https://www.euroncap.com/media/85820/euro-ncap-protocol-safe-driving-occupant-monitoring-v10.pdf
DSM测试场景完整清单
1. 分心检测场景(Distraction)
| 场景ID | 描述 | 触发条件 | 检测时限 | 警告等级 |
|---|---|---|---|---|
| D-01 | 视线偏离道路 | 视线离开前方 ≥3秒 | ≤3秒 | 一级警告 |
| D-02 | 手机使用(耳边) | 手持手机至耳边 | ≤2秒 | 一级警告 |
| D-03 | 手机使用(打字) | 低头看手机操作 | ≤2秒 | 二级警告 |
| D-04 | 调整中控 | 视线看中控屏幕 ≥3秒 | ≤3秒 | 一级警告 |
| D-05 | 饮食行为 | 手持食物/饮料 | ≤4秒 | 一级警告 |
| D-06 | 吸烟行为 | 手持香烟/电子烟 | ≤5秒 | 一级警告 |
| D-07 | 后视镜过度查看 | 看后视镜 ≥5秒 | ≤5秒 | 一级警告 |
| D-08 | 乘客交谈分心 | 头部转向乘客 ≥3秒 | ≤3秒 | 一级警告 |
2. 疲劳检测场景(Fatigue)
| 场景ID | 描述 | 触发条件 | 检测时限 | 警告等级 |
|---|---|---|---|---|
| F-01 | PERCLOS超标 | PERCLOS ≥30% 持续5秒 | ≤5秒 | 二级警告 |
| F-02 | 微睡眠 | 单次闭眼 ≥1.5秒 | ≤3秒 | 一级警告 |
| F-03 | 频繁眨眼 | 眨眼频率 ≥25 bpm 持续10秒 | ≤10秒 | 一级警告 |
| F-04 | 打哈欠 | 检测到打哈欠 | ≤2秒 | 一级警告 |
| F-05 | 头部下垂 | 头部俯仰角 ≥20° 持续3秒 | ≤3秒 | 二级警告 |
3. OMS测试场景(Occupant Monitoring)
| 场景ID | 描述 | 检测要求 | 警告条件 |
|---|---|---|---|
| O-01 | 儿童存在检测 | 检测车内儿童 | 锁车后≤60秒 |
| O-02 | 乘员前倾检测 | 检测前倾姿态 | 距仪表板≤20cm |
| O-03 | 安全带使用 | 检测安全带状态 | 未系+座椅有人 |
| O-04 | 儿童座椅检测 | 识别儿童座椅类型 | 错误安装警告 |
4. CPD测试场景(Child Presence Detection)
| 场景ID | 描述 | 测试对象 | 检测时限 |
|---|---|---|---|
| CPD-01 | 6个月婴儿 | 后向儿童座椅 | ≤60秒 |
| CPD-02 | 3岁儿童 | 前向儿童座椅 | ≤60秒 |
| CPD-03 | 儿童熟睡 | 轻微呼吸 | ≤60秒 |
| CPD-04 | 毛毯覆盖 | 完全覆盖 | ≤60秒 |
| CPD-05 | 宠物遗留 | 狗/猫在车内 | ≤60秒 |
| CPD-06 | 后备箱检测 | 儿童在后备箱 | ≤60秒 |
检测实现代码
1. 分心检测
import numpy as np
from typing import Dict, List, Tuple
from dataclasses import dataclass
from enum import Enum
class WarningLevel(Enum):
NONE = 0
LEVEL_1 = 1 # 声音警告
LEVEL_2 = 2 # 声音+震动警告
@dataclass
class DistractionEvent:
"""分心事件"""
scene_id: str
start_time: float
duration: float
warning_level: WarningLevel
detected: bool
class EuroNCAPDistractionDetector:
"""
Euro NCAP分心检测器
实现D-01到D-08场景
"""
def __init__(self):
# 场景配置
self.scenes = {
'D-01': {
'name': '视线偏离道路',
'threshold': 3.0, # 秒
'detection_time': 3.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_gaze_off_road
},
'D-02': {
'name': '手机使用(耳边)',
'threshold': 2.0,
'detection_time': 2.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_phone_ear
},
'D-03': {
'name': '手机使用(打字)',
'threshold': 2.0,
'detection_time': 2.0,
'warning_level': WarningLevel.LEVEL_2,
'condition': self._check_phone_typing
},
'D-04': {
'name': '调整中控',
'threshold': 3.0,
'detection_time': 3.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_infotainment
},
'D-05': {
'name': '饮食行为',
'threshold': 4.0,
'detection_time': 4.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_eating
},
'D-06': {
'name': '吸烟行为',
'threshold': 5.0,
'detection_time': 5.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_smoking
},
'D-07': {
'name': '后视镜过度查看',
'threshold': 5.0,
'detection_time': 5.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_mirror
},
'D-08': {
'name': '乘客交谈分心',
'threshold': 3.0,
'detection_time': 3.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_passenger_talk
}
}
# 状态追踪
self.active_events: Dict[str, float] = {} # scene_id -> start_time
def process_frame(self,
gaze_direction: Tuple[float, float],
head_pose: Tuple[float, float, float],
hand_positions: List[Tuple[float, float]],
timestamp: float) -> List[DistractionEvent]:
"""
处理单帧
Args:
gaze_direction: 视线方向 (x, y) -1到1
head_pose: 头部姿态 (roll, pitch, yaw) 度
hand_positions: 手部位置列表
timestamp: 时间戳
Returns:
events: 检测到的分心事件
"""
events = []
# 构造检测上下文
context = {
'gaze': gaze_direction,
'head_pose': head_pose,
'hands': hand_positions,
'timestamp': timestamp
}
# 检查每个场景
for scene_id, config in self.scenes.items():
triggered = config['condition'](context)
if triggered:
if scene_id not in self.active_events:
# 新事件
self.active_events[scene_id] = timestamp
else:
# 检查是否达到阈值
duration = timestamp - self.active_events[scene_id]
if duration >= config['threshold']:
events.append(DistractionEvent(
scene_id=scene_id,
start_time=self.active_events[scene_id],
duration=duration,
warning_level=config['warning_level'],
detected=True
))
else:
# 清除事件
if scene_id in self.active_events:
del self.active_events[scene_id]
return events
def _check_gaze_off_road(self, ctx: Dict) -> bool:
"""检查D-01:视线偏离道路"""
x, y = ctx['gaze']
# 道路区域:前方-30°到+30°
return abs(x) > 0.3 or abs(y) > 0.3
def _check_phone_ear(self, ctx: Dict) -> bool:
"""检查D-02:手机耳边"""
# 检查手部位置靠近耳朵
for hand in ctx['hands']:
hx, hy = hand
if 0.6 < hx < 0.9 and 0.3 < hy < 0.7: # 耳朵区域
return True
return False
def _check_phone_typing(self, ctx: Dict) -> bool:
"""检查D-03:手机打字"""
# 检查低头+手在胸前
pitch = ctx['head_pose'][1]
hands_low = any(0.3 < h[1] < 0.6 for h in ctx['hands'])
return pitch > 20 and hands_low
def _check_infotainment(self, ctx: Dict) -> bool:
"""检查D-04:调整中控"""
x, y = ctx['gaze']
# 中控区域:右下方
return 0.4 < x < 0.8 and y > 0.2
def _check_eating(self, ctx: Dict) -> bool:
"""检查D-05:饮食"""
# 手部在嘴部区域
for hand in ctx['hands']:
if 0.4 < hand[0] < 0.6 and 0.4 < hand[1] < 0.6:
return True
return False
def _check_smoking(self, ctx: Dict) -> bool:
"""检查D-06:吸烟"""
# 类似D-05,但手部位置稍有不同
for hand in ctx['hands']:
if 0.45 < hand[0] < 0.55 and 0.35 < hand[1] < 0.45:
return True
return False
def _check_mirror(self, ctx: Dict) -> bool:
"""检查D-07:后视镜"""
yaw = ctx['head_pose'][2]
return abs(yaw) > 30 # 头部转向>30°
def _check_passenger_talk(self, ctx: Dict) -> bool:
"""检查D-08:乘客交谈"""
yaw = ctx['head_pose'][2]
x, y = ctx['gaze']
# 头部转向+视线偏离
return abs(yaw) > 45 and abs(x) > 0.4
# ============ 疲劳检测 ============
class EuroNCAPFatigueDetector:
"""
Euro NCAP疲劳检测器
实现F-01到F-05场景
"""
def __init__(self):
self.scenes = {
'F-01': {
'name': 'PERCLOS超标',
'threshold': 30, # %
'duration': 5.0,
'warning_level': WarningLevel.LEVEL_2,
'condition': self._check_perclos
},
'F-02': {
'name': '微睡眠',
'threshold': 1.5, # 秒
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_microsleep
},
'F-03': {
'name': '频繁眨眼',
'threshold': 25, # bpm
'duration': 10.0,
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_blink_rate
},
'F-04': {
'name': '打哈欠',
'warning_level': WarningLevel.LEVEL_1,
'condition': self._check_yawn
},
'F-05': {
'name': '头部下垂',
'threshold': 20, # 度
'duration': 3.0,
'warning_level': WarningLevel.LEVEL_2,
'condition': self._check_head_drop
}
}
# 历史数据
self.eye_history: List[Tuple[float, float]] = [] # (timestamp, openness)
self.blink_times: List[float] = []
def process_frame(self,
eye_openness: Tuple[float, float],
head_pose: Tuple[float, float, float],
mouth_openness: float,
timestamp: float) -> List[Dict]:
"""
处理单帧
Args:
eye_openness: 左右眼开度 (left, right)
head_pose: 头部姿态
mouth_openness: 嘴巴开度
timestamp: 时间戳
Returns:
events: 疲劳事件列表
"""
events = []
# 更新历史
avg_eye = np.mean(eye_openness)
self.eye_history.append((timestamp, avg_eye))
# 检测眨眼
self._detect_blink(avg_eye, timestamp)
# 检查各场景
for scene_id, config in self.scenes.items():
result = config['condition']({
'eye_openness': avg_eye,
'head_pose': head_pose,
'mouth_openness': mouth_openness,
'timestamp': timestamp,
'history': self.eye_history,
'blink_times': self.blink_times
})
if result:
events.append({
'scene_id': scene_id,
'name': config['name'],
'warning_level': config['warning_level'].value
})
# 清理历史(保留60秒)
self.eye_history = [(t, e) for t, e in self.eye_history
if timestamp - t < 60]
return events
def _detect_blink(self, eye_openness: float, timestamp: float):
"""检测眨眼事件"""
if len(self.eye_history) > 1:
prev_openness = self.eye_history[-2][1]
# 从开到闭
if prev_openness > 0.3 and eye_openness < 0.2:
self.blink_times.append(timestamp)
# 清理旧记录
self.blink_times = [t for t in self.blink_times
if timestamp - t < 60]
def _check_perclos(self, ctx: Dict) -> bool:
"""检查F-01:PERCLOS"""
history = ctx['history']
timestamp = ctx['timestamp']
if len(history) < 60: # 至少2秒@30fps
return False
# 最近60帧
recent = [(t, e) for t, e in history
if timestamp - t <= 60]
if len(recent) < 60:
return False
# 计算PERCLOS
closed_count = sum(1 for _, e in recent if e < 0.2)
perclos = closed_count / len(recent) * 100
return perclos >= 30
def _check_microsleep(self, ctx: Dict) -> bool:
"""检查F-02:微睡眠"""
history = ctx['history']
if len(history) < 45: # 1.5秒
return False
# 检查最近1.5秒是否全部闭眼
recent = [e for _, e in history[-45:]]
return all(e < 0.2 for e in recent)
def _check_blink_rate(self, ctx: Dict) -> bool:
"""检查F-03:频繁眨眼"""
blink_times = ctx['blink_times']
timestamp = ctx['timestamp']
if len(blink_times) < 3:
return False
# 最近10秒的眨眼
recent = [t for t in blink_times if timestamp - t <= 10]
if len(recent) < 2:
return False
# 计算频率
duration = timestamp - recent[0]
if duration < 5:
return False
rate = len(recent) / duration * 60 # bpm
return rate >= 25
def _check_yawn(self, ctx: Dict) -> bool:
"""检查F-04:打哈欠"""
mouth = ctx['mouth_openness']
return mouth > 0.7 # 嘴巴大张
def _check_head_drop(self, ctx: Dict) -> bool:
"""检查F-05:头部下垂"""
pitch = ctx['head_pose'][1]
return abs(pitch) >= 20
# ============ Euro NCAP测试执行器 ============
class EuroNCAPTestExecutor:
"""
Euro NCAP测试执行器
模拟官方测试流程
"""
def __init__(self):
self.distraction_detector = EuroNCAPDistractionDetector()
self.fatigue_detector = EuroNCAPFatigueDetector()
# 测试结果
self.results = {
'distraction': {},
'fatigue': {}
}
def run_distraction_test(self, scene_id: str, duration: float = 30.0) -> Dict:
"""
运行分心测试
Args:
scene_id: 场景ID (D-01到D-08)
duration: 测试时长(秒)
Returns:
result: 测试结果
"""
# 模拟测试数据
# 实际测试需使用官方测试轨道和场景
fps = 30
n_frames = int(duration * fps)
detection_time = None
warning_issued = False
for i in range(n_frames):
timestamp = i / fps
# 根据场景生成测试数据
gaze, head, hands = self._generate_test_data(scene_id, i)
events = self.distraction_detector.process_frame(
gaze, head, hands, timestamp
)
if events and detection_time is None:
detection_time = timestamp
warning_issued = True
break
# 评估结果
config = self.distraction_detector.scenes[scene_id]
passed = detection_time is not None and \
detection_time <= config['detection_time']
return {
'scene_id': scene_id,
'name': config['name'],
'detection_time': detection_time,
'expected_time': config['detection_time'],
'passed': passed,
'warning_level': config['warning_level'].value
}
def _generate_test_data(self, scene_id: str, frame: int) -> Tuple:
"""生成测试数据(简化版)"""
# 实际测试应使用真实场景
if scene_id == 'D-01':
# 视线偏离
return (0.5, 0.0), (0, 0, 0), []
elif scene_id == 'D-02':
# 手机耳边
return (0.0, 0.0), (0, 0, 10), [(0.7, 0.5)]
else:
# 默认正常
return (0.1, 0.0), (0, 0, 0), []
def generate_report(self) -> str:
"""生成测试报告"""
report = []
report.append("=" * 60)
report.append("Euro NCAP 2026 DSM测试报告")
report.append("=" * 60)
# 分心测试结果
report.append("\n分心检测场景(D-01到D-08):")
report.append("-" * 40)
for scene_id in ['D-01', 'D-02', 'D-03', 'D-04', 'D-05', 'D-06', 'D-07', 'D-08']:
result = self.run_distraction_test(scene_id)
status = "✓ 通过" if result['passed'] else "✗ 失败"
report.append(f"{scene_id}: {result['name']} - {status}")
if result['detection_time']:
report.append(f" 检测时间: {result['detection_time']:.2f}s (要求≤{result['expected_time']:.1f}s)")
return "\n".join(report)
# ============ 实际测试 ============
if __name__ == "__main__":
# 运行测试
executor = EuroNCAPTestExecutor()
# 分心检测测试
print("=" * 60)
print("Euro NCAP 2026 DSM测试")
print("=" * 60)
# 测试D-01
print("\n场景D-01测试:视线偏离道路")
print("-" * 40)
detector = EuroNCAPDistractionDetector()
# 模拟5秒视线偏离
for i in range(150): # 5秒@30fps
events = detector.process_frame(
gaze_direction=(0.5, 0.0), # 视线偏离
head_pose=(0, 0, 0),
hand_positions=[],
timestamp=i / 30
)
if events:
print(f"检测到分心!场景: {events[0].scene_id}")
print(f"检测时间: {events[0].start_time:.2f}s")
print(f"警告等级: {events[0].warning_level.name}")
break
# 生成完整报告
print("\n" + executor.generate_report())
Euro NCAP 2026 DSM测试场景详解:完整清单与检测要求
https://dapalm.com/2026/04/25/2026-04-25-euro-ncap-dsm-test-scenarios-complete/