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/
作者
Mars
发布于
2026年4月25日
许可协议