Euro NCAP 2026 驾驶员分心检测技术详解与代码实现

Euro NCAP 2026 驾驶员分心检测技术详解与代码实现

Euro NCAP 2026 分心检测标准

Euro NCAP 2026 对分心检测提出了极高的技术要求,从简单的”看没看路”进化到复杂的时间窗口分析:

分心检测类型

类型 检测条件 技术难度
长分心 单次注视偏离 3-4 秒 ⭐⭐
短分心(VATS) 30秒内累积 10 秒注视偏离 ⭐⭐⭐⭐⭐
手机使用 分类 Basic/Advanced 手机使用 ⭐⭐⭐⭐

VATS(Visual Attention Time Sharing)详解

定义:30 秒时间窗口内,驾驶员视线偏离道路的累积时间达到 10 秒。

技术挑战

  1. 需要维护滑动时间窗口
  2. 实时计算累积分心时间
  3. 区分不同类型的视线偏离(看后视镜 vs 看手机)
  4. 排除合法的视线偏离(观察路况)

注视区域分类

Euro NCAP 定义的注视区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────┐
│ │
│ ┌─────┐ ┌─────┐ │
│ │左侧窗│ │右侧窗│ │
│ │ 后视镜 │ 后视镜 │
│ └─────┘ └─────┘ │
│ │
│ ┌───────────────────────┐ │
│ │ 前风挡/道路 │ │
│ │ (合法注视区域) │ │
│ └───────────────────────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │仪表盘│ │中控屏│ │手套箱│ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 脚部区域 / 膝盖区域 │ │
│ │ (手机检测重点区域) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────┘

注视区域判定逻辑

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
import numpy as np
from typing import Tuple, List
from enum import Enum

class GazeRegion(Enum):
"""Euro NCAP 注视区域枚举"""
ROAD_FORWARD = "road_forward" # 前方道路(合法)
DRIVER_SIDE_WINDOW = "driver_side_window" # 驾驶员侧窗
PASSENGER_SIDE_WINDOW = "passenger_side_window" # 副驾侧窗
DRIVER_SIDE_MIRROR = "driver_side_mirror" # 左后视镜
PASSENGER_SIDE_MIRROR = "passenger_side_mirror" # 右后视镜
REAR_MIRROR = "rear_mirror" # 中央后视镜
INSTRUMENT_CLUSTER = "instrument_cluster" # 仪表盘
CENTER_STACK = "center_stack" # 中控屏
GLOVEBOX = "glovebox" # 手套箱
DRIVER_FOOTWELL = "driver_footwell" # 驾驶员脚部
DRIVER_LAP = "driver_lap" # 驾驶员膝盖/大腿
PHONE_DASHBOARD = "phone_dashboard" # 仪表台上的手机
PHONE_LAP = "phone_lap" # 膝盖上的手机
PASSENGER_FACE = "passenger_face" # 乘客面部


class GazeRegionClassifier:
"""
注视区域分类器

基于头部姿态和眼动向量判定驾驶员注视区域
"""

def __init__(self):
# 相机安装位置参数(假设安装在仪表台上方)
self.camera_position = np.array([0.3, -0.1, 1.2]) # 相对驾驶员参考点

# 各区域的球面角度范围(yaw, pitch)
self.region_bounds = {
GazeRegion.ROAD_FORWARD: {
'yaw': (-30, 30), # ±30度
'pitch': (-20, 10), # 向下20度到向上10度
'valid': True # 合法注视区域
},
GazeRegion.DRIVER_SIDE_WINDOW: {
'yaw': (-90, -45),
'pitch': (-10, 20),
'valid': True # 观察路况
},
GazeRegion.PASSENGER_SIDE_WINDOW: {
'yaw': (45, 90),
'pitch': (-10, 20),
'valid': True # 观察路况
},
GazeRegion.DRIVER_SIDE_MIRROR: {
'yaw': (-35, -25),
'pitch': (-15, -5),
'valid': True # 后视镜观察
},
GazeRegion.PASSENGER_SIDE_MIRROR: {
'yaw': (55, 65),
'pitch': (-15, -5),
'valid': True # 后视镜观察
},
GazeRegion.REAR_MIRROR: {
'yaw': (-15, 15),
'pitch': (-25, -15),
'valid': True # 后视镜观察
},
GazeRegion.INSTRUMENT_CLUSTER: {
'yaw': (-25, -5),
'pitch': (-35, -20),
'valid': False # 分心区域
},
GazeRegion.CENTER_STACK: {
'yaw': (-5, 35),
'pitch': (-40, -25),
'valid': False # 分心区域
},
GazeRegion.GLOVEBOX: {
'yaw': (30, 50),
'pitch': (-50, -35),
'valid': False # 分心区域
},
GazeRegion.DRIVER_FOOTWELL: {
'yaw': (-20, 0),
'pitch': (-70, -50),
'valid': False # 分心区域
},
GazeRegion.DRIVER_LAP: {
'yaw': (-30, 10),
'pitch': (-50, -35),
'valid': False # 分心区域(可能使用手机)
},
GazeRegion.PASSENGER_FACE: {
'yaw': (20, 50),
'pitch': (-10, 10),
'valid': False # 分心区域
}
}

def classify(self, head_yaw: float, head_pitch: float,
gaze_yaw_offset: float, gaze_pitch_offset: float) -> Tuple[GazeRegion, bool]:
"""
分类注视区域

Args:
head_yaw: 头部偏航角(度)
head_pitch: 头部俯仰角(度)
gaze_yaw_offset: 眼动相对头部的偏航偏移(度)
gaze_pitch_offset: 眼动相对头部的俯仰偏移(度)

Returns:
region: 注视区域
is_valid: 是否为合法注视区域
"""
# 计算总注视角度
total_yaw = head_yaw + gaze_yaw_offset
total_pitch = head_pitch + gaze_pitch_offset

# 遍历区域边界匹配
for region, bounds in self.region_bounds.items():
yaw_min, yaw_max = bounds['yaw']
pitch_min, pitch_max = bounds['pitch']

if (yaw_min <= total_yaw <= yaw_max and
pitch_min <= total_pitch <= pitch_max):
return region, bounds['valid']

# 默认:未知区域,视为分心
return GazeRegion.ROAD_FORWARD, True # 保守策略

VATS 分心检测算法实现

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
from collections import deque
from dataclasses import dataclass
from typing import Optional
import time

@dataclass
class GazeEvent:
"""注视事件"""
timestamp: float # 时间戳
region: GazeRegion # 注视区域
is_distraction: bool # 是否为分心
duration: float = 0.0 # 持续时间


class VATSDetector:
"""
VATS (Visual Attention Time Sharing) 分心检测器

Euro NCAP 2026 要求:
- 30秒时间窗口
- 累积分心时间 ≥10秒触发警告
"""

def __init__(self, window_size_sec: float = 30.0,
distraction_threshold_sec: float = 10.0):
self.window_size_sec = window_size_sec
self.distraction_threshold_sec = distraction_threshold_sec

# 滑动窗口存储注视事件
self.gaze_events: deque = deque()

# 当前注视状态
self.current_gaze_start: Optional[float] = None
self.current_region: Optional[GazeRegion] = None
self.current_is_distraction: bool = False

# VATS 状态
self.accumulated_distraction: float = 0.0
self.vats_triggered: bool = False

def update(self, region: GazeRegion, is_distraction: bool,
timestamp: Optional[float] = None) -> dict:
"""
更新 VATS 状态

Args:
region: 当前注视区域
is_distraction: 是否为分心区域
timestamp: 时间戳(默认当前时间)

Returns:
status: VATS 状态
"""
if timestamp is None:
timestamp = time.time()

# 检测注视区域变化
if region != self.current_region:
# 结束当前注视
if self.current_gaze_start is not None:
duration = timestamp - self.current_gaze_start
self._add_gaze_event(
GazeEvent(
timestamp=self.current_gaze_start,
region=self.current_region,
is_distraction=self.current_is_distraction,
duration=duration
)
)

# 开始新注视
self.current_gaze_start = timestamp
self.current_region = region
self.current_is_distraction = is_distraction

# 计算当前累积分心时间
self._update_accumulated_distraction(timestamp)

# 检测 VATS 触发
self.vats_triggered = self.accumulated_distraction >= self.distraction_threshold_sec

return {
'accumulated_distraction': self.accumulated_distraction,
'vats_triggered': self.vats_triggered,
'window_remaining': self.window_size_sec - self.accumulated_distraction,
'current_region': region.value,
'current_is_distraction': is_distraction
}

def _add_gaze_event(self, event: GazeEvent):
"""添加注视事件到滑动窗口"""
# 移除窗口外的事件
cutoff_time = event.timestamp - self.window_size_sec
while self.gaze_events and self.gaze_events[0].timestamp < cutoff_time:
self.gaze_events.popleft()

# 添加新事件
self.gaze_events.append(event)

def _update_accumulated_distraction(self, current_time: float):
"""计算累积分心时间"""
# 计算窗口内分心事件的总时长
cutoff_time = current_time - self.window_size_sec

total_distraction = 0.0
for event in self.gaze_events:
if event.is_distraction and event.timestamp >= cutoff_time:
total_distraction += event.duration

# 加上当前进行中的分心
if self.current_is_distraction and self.current_gaze_start is not None:
ongoing_duration = current_time - self.current_gaze_start
total_distraction += ongoing_duration

self.accumulated_distraction = total_distraction

def reset(self):
"""重置 VATS 状态"""
self.gaze_events.clear()
self.current_gaze_start = None
self.current_region = None
self.current_is_distraction = False
self.accumulated_distraction = 0.0
self.vats_triggered = False


# 测试代码
if __name__ == "__main__":
classifier = GazeRegionClassifier()
vats_detector = VATSDetector(window_size_sec=30.0, distraction_threshold_sec=10.0)

# 模拟注视序列
test_sequence = [
# (yaw, pitch, gaze_yaw_offset, gaze_pitch_offset, duration_sec)
(0, 0, 0, 0, 5.0), # 前方道路 5秒
(-50, 0, 0, 0, 2.0), # 左侧窗 2秒
(0, 0, 0, 0, 3.0), # 前方道路 3秒
(0, -40, 0, 0, 4.0), # 中控屏 4秒(分心)
(0, 0, 0, 0, 2.0), # 前方道路 2秒
(0, -40, 0, 0, 5.0), # 中控屏 5秒(分心)
(0, 0, 0, 0, 1.0), # 前方道路 1秒
(-25, -30, 0, 0, 4.0), # 仪表盘 4秒(分心)
]

current_time = 0.0
print("VATS 分心检测测试")
print("=" * 60)

for yaw, pitch, gaze_yaw, gaze_pitch, duration in test_sequence:
region, is_valid = classifier.classify(yaw, pitch, gaze_yaw, gaze_pitch)

# 模拟注视该区域 duration 秒
end_time = current_time + duration
while current_time < end_time:
status = vats_detector.update(region, not is_valid, current_time)
current_time += 0.1 # 100ms 更新间隔

print(f"注视区域: {region.value:25s} | "
f"分心: {not is_valid:5s} | "
f"持续: {duration:.1f}s | "
f"累积分心: {status['accumulated_distraction']:.1f}s | "
f"VATS触发: {status['vats_triggered']}")

print("=" * 60)
print(f"最终累积分心时间: {vats_detector.accumulated_distraction:.1f}秒")
print(f"VATS 触发状态: {vats_detector.vats_triggered}")

长分心检测实现

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
class LongDistractionDetector:
"""
长分心检测器

Euro NCAP 2026 要求:
- 单次注视偏离 ≥3秒触发警告
- 必须先有 4秒前方注视才能检测长分心
"""

def __init__(self,
min_road_gaze_sec: float = 4.0,
distraction_threshold_sec: float = 3.0,
max_distraction_sec: float = 4.0):
self.min_road_gaze_sec = min_road_gaze_sec
self.distraction_threshold_sec = distraction_threshold_sec
self.max_distraction_sec = max_distraction_sec

# 状态跟踪
self.consecutive_road_gaze: float = 0.0
self.current_distraction_start: Optional[float] = None
self.current_distraction_region: Optional[GazeRegion] = None
self.distraction_triggered: bool = False

def update(self, region: GazeRegion, is_distraction: bool,
timestamp: float) -> dict:
"""
更新长分心状态

Args:
region: 当前注视区域
is_distraction: 是否为分心区域
timestamp: 当前时间戳

Returns:
status: 长分心状态
"""
if is_distraction:
# 进入分心状态
if self.consecutive_road_gaze >= self.min_road_gaze_sec:
# 满足前置条件,开始计时
if self.current_distraction_start is None:
self.current_distraction_start = timestamp
self.current_distraction_region = region

# 计算分心持续时间
distraction_duration = timestamp - self.current_distraction_start

# 检测触发
if distraction_duration >= self.distraction_threshold_sec:
self.distraction_triggered = True

return {
'type': 'distraction',
'duration': distraction_duration,
'threshold': self.distraction_threshold_sec,
'triggered': self.distraction_triggered,
'region': region.value
}
else:
# 前置条件不满足,不检测
return {
'type': 'distraction',
'duration': 0.0,
'threshold': self.distraction_threshold_sec,
'triggered': False,
'region': region.value,
'reason': f'前置道路注视不足 {self.min_road_gaze_sec}秒'
}
else:
# 看向道路
self.consecutive_road_gaze += 0.1 # 假设 100ms 更新间隔

# 重置分心状态
self.current_distraction_start = None
self.current_distraction_region = None
self.distraction_triggered = False

return {
'type': 'road_gaze',
'consecutive_duration': self.consecutive_road_gaze,
'triggered': False
}

手机使用检测

Euro NCAP 手机使用分类

类型 描述 运动分类
Basic Phone Use 基础手机使用 Owl(头部移动)或 Lizard(眼动)
Advanced Phone Use 高级手机使用(打字等) Lizard(眼动)
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
class PhoneUseDetector:
"""
手机使用检测器

Euro NCAP 2026 要求:
- 检测手机位置和交互类型
- 区分 Basic/Advanced 手机使用
"""

def __init__(self):
# 手机可能的位置
self.phone_locations = [
GazeRegion.PHONE_DASHBOARD,
GazeRegion.PHONE_LAP,
GazeRegion.DRIVER_FOOTWELL,
GazeRegion.DRIVER_LAP
]

# 手部检测阈值
self.hand_near_face_threshold = 0.15 # 手接近面部的距离阈值
self.typing_motion_threshold = 0.05 # 打字动作的移动阈值

def detect(self, gaze_region: GazeRegion,
head_movement: np.ndarray,
hand_positions: List[np.ndarray],
timestamp: float) -> dict:
"""
检测手机使用

Args:
gaze_region: 当前注视区域
head_movement: 头部运动向量
hand_positions: 手部关键点位置列表
timestamp: 时间戳

Returns:
detection: 手机使用检测结果
"""
# 1. 检查注视区域是否涉及手机
is_phone_gaze = gaze_region in self.phone_locations

# 2. 检测手部位置
hand_near_lap = self._check_hand_near_lap(hand_positions)
hand_near_face = self._check_hand_near_face(hand_positions)

# 3. 检测打字动作
is_typing = self._detect_typing_motion(hand_positions)

# 4. 分类手机使用类型
if is_phone_gaze or hand_near_lap or hand_near_face:
if is_typing:
phone_use_type = 'advanced' # 打字等高级交互
else:
phone_use_type = 'basic' # 基础查看

# 运动分类
if np.linalg.norm(head_movement) > 0.1:
movement_type = 'owl' # 头部主导
else:
movement_type = 'lizard' # 眼动主导

return {
'phone_use_detected': True,
'phone_use_type': phone_use_type,
'movement_type': movement_type,
'gaze_region': gaze_region.value,
'hand_near_face': hand_near_face,
'is_typing': is_typing
}

return {
'phone_use_detected': False
}

def _check_hand_near_lap(self, hand_positions: List[np.ndarray]) -> bool:
"""检查手是否在膝盖区域"""
# 简化实现:检查手部 y 坐标是否在膝盖区域
if not hand_positions:
return False

for hand_pos in hand_positions:
# 假设 y 坐标 0.3-0.6 为膝盖区域
if 0.3 < hand_pos[1] < 0.6:
return True
return False

def _check_hand_near_face(self, hand_positions: List[np.ndarray]) -> bool:
"""检查手是否接近面部"""
if not hand_positions:
return False

face_center = np.array([0.5, 0.3]) # 假设面部中心

for hand_pos in hand_positions:
distance = np.linalg.norm(hand_pos[:2] - face_center)
if distance < self.hand_near_face_threshold:
return True
return False

def _detect_typing_motion(self, hand_positions: List[np.ndarray]) -> bool:
"""检测打字动作(简化版)"""
# 需要多帧手部位置来检测动作
# 这里返回简化的结果
return False

IMS 开发启示

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
51
52
53
54
55
56
# 完整的分心检测管道
class DistractionDetectionPipeline:
"""分心检测完整管道"""

def __init__(self):
self.region_classifier = GazeRegionClassifier()
self.vats_detector = VATSDetector()
self.long_distraction_detector = LongDistractionDetector()
self.phone_detector = PhoneUseDetector()

def process_frame(self,
head_pose: Tuple[float, float, float],
gaze_vector: Tuple[float, float],
hand_keypoints: List[np.ndarray],
timestamp: float) -> dict:
"""
处理单帧数据

Args:
head_pose: (yaw, pitch, roll) 头部姿态
gaze_vector: (yaw, pitch) 注视向量
hand_keypoints: 手部关键点
timestamp: 时间戳

Returns:
result: 分心检测结果
"""
# 1. 分类注视区域
region, is_valid = self.region_classifier.classify(
head_pose[0], head_pose[1],
gaze_vector[0], gaze_vector[1]
)

# 2. VATS 检测
vats_status = self.vats_detector.update(region, not is_valid, timestamp)

# 3. 长分心检测
long_distraction_status = self.long_distraction_detector.update(
region, not is_valid, timestamp
)

# 4. 手机使用检测
phone_status = self.phone_detector.detect(
region,
np.array(head_pose),
hand_keypoints,
timestamp
)

return {
'gaze_region': region.value,
'is_distraction': not is_valid,
'vats': vats_status,
'long_distraction': long_distraction_status,
'phone_use': phone_status
}

2. 性能优化建议

优化点 方法 效果
注视区域分类 使用查找表替代遍历 减少 80% 计算量
VATS 滑动窗口 使用循环缓冲区 O(1) 插入/删除
长分心检测 状态机实现 减少分支判断
手机检测 仅在可疑区域激活 减少误检

3. Euro NCAP 合规测试场景

场景 测试内容 通过条件
L-01 长分心 3 秒 ≤3.5 秒触发警告
L-02 长分心 4 秒 ≤4.5 秒触发警告
VATS-01 累积分心 10 秒 ≤10.5 秒触发警告
P-01 基础手机使用 ≤3 秒检测并分类
P-02 高级手机使用 ≤3 秒检测并分类

参考来源:


Euro NCAP 2026 驾驶员分心检测技术详解与代码实现
https://dapalm.com/2026/06/13/2026-06-13-Euro-NCAP-2026-Distraction-Detection-VATS/
作者
Mars
发布于
2026年6月13日
许可协议