Euro NCAP 2026 后排乘员监控深度解析:安全带误用检测、气囊自适应与 OOP 完整实现指南

一、Euro NCAP 2026 后排乘员监控要求

1.1 法规背景

Euro NCAP 2026 将乘员监控(Occupant Monitoring System, OMS)从驾驶员扩展到全车座椅,后排监控成为新增评分项。根据 Euro NCAP Safe Driving Occupant Monitoring Protocol v1.0(2025年3月),系统必须:

监控能力 法规要求 分值
后排乘员检测 所有后排座椅 5分
安全带状态检测 系好/未系/误用 5分
儿童座椅识别 前向/后向/增高座椅 3分
异常姿态检测 脚踩仪表板/身体前倾 3分
乘员体型分类 5%/50%/95%百分位 3分

1.2 安全带误用检测

Euro NCAP 2026 首次要求检测安全带误用(Seatbelt Misuse),必须在 30 秒内发出警告:

误用类型 描述 分值
仅扣安全带扣 安全带扣扣上,但安全带未穿过身体 2分
仅腰部安全带 肩部安全带放在背后 2分
完全在背后 整条安全带放在背后 1分

1.3 气囊自适应要求

乘员类型 气囊状态要求
后向儿童座椅 气囊必须 OFF(自动或系统提示)
5%百分位及更大成人 气囊必须 ON
前向儿童座椅 根据体型判断

1.4 异常姿态检测(OOP)

异常姿态 检测要求 警告时限
脚踩仪表板 三个位置:内侧/中心线/外侧 30秒内
上身前倾 头部距离仪表板 <20cm 30秒内
持续警告 每15分钟重复警告 -

二、后排乘员检测完整实现

2.1 依赖安装

1
2
3
4
5
6
# requirements.txt
numpy>=1.21.0
opencv-python>=4.5.0
mediapipe>=0.10.0
onnxruntime>=1.16.0
scipy>=1.7.0

2.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
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
import numpy as np
import cv2
from typing import Tuple, Optional, List
from dataclasses import dataclass
from enum import Enum

class OccupantStatus(Enum):
"""乘员状态"""
EMPTY = "empty" # 座椅无人
OCCUPIED = "occupied" # 座椅有人
CHILD_SEAT = "child_seat" # 儿童座椅
UNKNOWN = "unknown"


@dataclass
class SeatInfo:
"""座椅信息"""
row: int # 排数(1=前排,2=后排,3=第三排)
position: int # 位置(1=左侧,2=中间,3=右侧)
status: OccupantStatus
confidence: float
bbox: Tuple[int, int, int, int] # (x1, y1, x2, y2)


class RearOccupantDetector:
"""
后排乘员检测器

使用目标检测模型检测后排座椅区域的人员
支持 YOLOv8/YOLOv11 等 ONNX 模型
"""

# 座椅区域定义(相对于图像的比例)
# 需要根据实际摄像头安装位置标定
SEAT_ZONES = {
# (row, position): (x_ratio_min, y_ratio_min, x_ratio_max, y_ratio_max)
(2, 1): (0.0, 0.3, 0.33, 0.9), # 后排左侧
(2, 2): (0.33, 0.3, 0.67, 0.9), # 后排中间
(2, 3): (0.67, 0.3, 1.0, 0.9), # 后排右侧
}

def __init__(self,
model_path: str = "yolov8n.onnx",
conf_threshold: float = 0.5,
nms_threshold: float = 0.45,
frame_width: int = 1280,
frame_height: int = 720):
"""
初始化后排乘员检测器

Args:
model_path: ONNX 模型路径
conf_threshold: 置信度阈值
nms_threshold: NMS 阈值
frame_width: 图像宽度
frame_height: 图像高度
"""
self.conf_threshold = conf_threshold
self.nms_threshold = nms_threshold
self.frame_width = frame_width
self.frame_height = frame_height

# 加载 ONNX 模型
self.session = cv2.dnn.readNetFromONNX(model_path)
self.session.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
self.session.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)

# 类别标签(COCO 数据集)
self.class_names = ['person', 'bicycle', 'car', ...] # 省略完整列表
self.person_class_id = 0

def detect(self, frame: np.ndarray) -> List[SeatInfo]:
"""
检测后排座椅乘员

Args:
frame: BGR 图像,shape=(H, W, 3)

Returns:
座椅信息列表
"""
h, w = frame.shape[:2]

# 预处理
blob = cv2.dnn.blobFromImage(
frame, 1/255.0, (640, 640),
swapRB=True, crop=False
)
self.session.setInput(blob)

# 推理
outputs = self.session.forward()

# 后处理
detections = self._postprocess(outputs, w, h)

# 分配到座椅区域
seat_infos = self._assign_to_seats(detections, w, h)

return seat_infos

def _postprocess(self, outputs: np.ndarray,
frame_w: int, frame_h: int) -> List[dict]:
"""
后处理:解析检测结果

Args:
outputs: 模型输出
frame_w: 图像宽度
frame_h: 图像高度

Returns:
检测结果列表
"""
detections = []

# YOLOv8 输出格式: (1, 84, 8400)
# 84 = 4(bbox) + 80(classes)
outputs = outputs[0].transpose(1, 0) # (8400, 84)

for detection in outputs:
# 提取边界框和类别分数
x, y, w, h = detection[:4]
class_scores = detection[4:]

# 获取最高分类别
class_id = np.argmax(class_scores)
confidence = class_scores[class_id]

# 只保留人员类别
if class_id != self.person_class_id:
continue

if confidence < self.conf_threshold:
continue

# 转换坐标
x1 = int((x - w/2) * frame_w / 640)
y1 = int((y - h/2) * frame_h / 640)
x2 = int((x + w/2) * frame_w / 640)
y2 = int((y + h/2) * frame_h / 640)

detections.append({
'bbox': (x1, y1, x2, y2),
'confidence': confidence,
'class_id': class_id
})

return detections

def _assign_to_seats(self, detections: List[dict],
frame_w: int, frame_h: int) -> List[SeatInfo]:
"""
将检测结果分配到座椅区域

Args:
detections: 检测结果列表
frame_w: 图像宽度
frame_h: 图像高度

Returns:
座椅信息列表
"""
seat_infos = []

for (row, position), zone in self.SEAT_ZONES.items():
x_min, y_min, x_max, y_max = zone

# 计算座椅区域边界(像素)
seat_x1 = int(x_min * frame_w)
seat_y1 = int(y_min * frame_h)
seat_x2 = int(x_max * frame_w)
seat_y2 = int(y_max * frame_h)

# 查找该区域内的检测
best_detection = None
best_iou = 0.0

for det in detections:
# 计算 IoU
iou = self._calculate_iou(
det['bbox'],
(seat_x1, seat_y1, seat_x2, seat_y2)
)

if iou > best_iou:
best_iou = iou
best_detection = det

# 确定状态
if best_detection is not None and best_iou > 0.1:
status = OccupantStatus.OCCUPIED
confidence = best_detection['confidence']
bbox = best_detection['bbox']
else:
status = OccupantStatus.EMPTY
confidence = 0.9
bbox = (seat_x1, seat_y1, seat_x2, seat_y2)

seat_info = SeatInfo(
row=row,
position=position,
status=status,
confidence=confidence,
bbox=bbox
)
seat_infos.append(seat_info)

return seat_infos

def _calculate_iou(self, box1: Tuple, box2: Tuple) -> float:
"""计算两个边界框的 IoU"""
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])

if x2 <= x1 or y2 <= y1:
return 0.0

intersection = (x2 - x1) * (y2 - y1)
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
union = area1 + area2 - intersection

return intersection / union if union > 0 else 0.0


# 测试代码
if __name__ == "__main__":
detector = RearOccupantDetector(model_path="yolov8n.onnx")
cap = cv2.VideoCapture(0)

print("按 'q' 退出")
while True:
ret, frame = cap.read()
if not ret:
break

seat_infos = detector.detect(frame)

for seat in seat_infos:
# 绘制座椅区域
x1, y1, x2, y2 = seat.bbox
color = (0, 255, 0) if seat.status == OccupantStatus.OCCUPIED else (128, 128, 128)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)

# 显示状态
label = f"R{seat.row}P{seat.position}: {seat.status.value}"
cv2.putText(frame, label, (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)

cv2.imshow("Rear Occupant Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()

2.3 安全带状态检测

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
import numpy as np
import cv2
from typing import Tuple, Optional
from dataclasses import dataclass
from enum import Enum

class SeatbeltStatus(Enum):
"""安全带状态"""
UNBUCKLED = "unbuckled" # 未扣
BUCKLED_CORRECT = "buckled_correct" # 正确佩戴
BUCKLE_ONLY = "buckle_only" # 仅扣安全带扣
LAP_ONLY = "lap_only" # 仅腰部安全带
BEHIND_BACK = "behind_back" # 在背后
UNKNOWN = "unknown"


@dataclass
class SeatbeltInfo:
"""安全带信息"""
seat_row: int
seat_position: int
status: SeatbeltStatus
confidence: float
belt_visible: bool # 安全带是否可见


class SeatbeltDetector:
"""
安全带状态检测器

检测逻辑:
1. 检测安全带扣传感器信号( buckle sensor)
2. 检测安全带带体(视觉)
3. 判断安全带路径是否正确

误用判断:
- 仅扣安全带扣:扣传感器 ON,但带体不可见
- 仅腰部安全带:带体仅出现在腰部区域
- 在背后:带体路径偏离正常位置
"""

# 安全带区域定义(相对于乘员边界框)
# 需要根据实际座椅位置标定
BELT_ZONES = {
'shoulder': {'x': (0.3, 0.7), 'y': (0.1, 0.5)}, # 肩部区域
'chest': {'x': (0.3, 0.7), 'y': (0.3, 0.6)}, # 胸部区域
'lap': {'x': (0.2, 0.8), 'y': (0.7, 0.95)} # 腰部区域
}

def __init__(self):
"""初始化安全带检测器"""
# 加载安全带分割模型(语义分割)
# 这里使用简化的颜色检测方法
self.belt_color_lower = np.array([0, 0, 150]) # 深色
self.belt_color_upper = np.array([180, 50, 255])

def detect(self, frame: np.ndarray,
occupant_bbox: Tuple[int, int, int, int],
buckle_sensor: bool = False) -> SeatbeltInfo:
"""
检测安全带状态

Args:
frame: BGR 图像
occupant_bbox: 乘员边界框 (x1, y1, x2, y2)
buckle_sensor: 安全带扣传感器状态

Returns:
SeatbeltInfo 对象
"""
x1, y1, x2, y2 = occupant_bbox
h, w = frame.shape[:2]

# 边界检查
x1, y1 = max(0, x1), max(0, y1)
x2, y2 = min(w, x2), min(h, y2)

if x2 <= x1 or y2 <= y1:
return SeatbeltInfo(
seat_row=0,
seat_position=0,
status=SeatbeltStatus.UNKNOWN,
confidence=0.0,
belt_visible=False
)

# 提取乘员区域
occupant_roi = frame[y1:y2, x1:x2]

# 检测安全带带体
belt_mask = self._detect_belt_body(occupant_roi)
belt_visible = np.sum(belt_mask) > 0

# 分析安全带路径
if buckle_sensor:
if not belt_visible:
# 扣传感器 ON,但带体不可见 → 仅扣安全带扣
status = SeatbeltStatus.BUCKLE_ONLY
confidence = 0.8
else:
# 分析带体分布
belt_distribution = self._analyze_belt_distribution(belt_mask)

if belt_distribution['shoulder'] > 0.3 and belt_distribution['chest'] > 0.3:
# 带体覆盖肩部和胸部 → 正确佩戴
status = SeatbeltStatus.BUCKLED_CORRECT
confidence = 0.9
elif belt_distribution['lap'] > 0.5 and belt_distribution['shoulder'] < 0.1:
# 带体仅覆盖腰部 → 仅腰部安全带
status = SeatbeltStatus.LAP_ONLY
confidence = 0.7
else:
# 其他情况 → 在背后
status = SeatbeltStatus.BEHIND_BACK
confidence = 0.6
else:
# 扣传感器 OFF
if belt_visible:
# 带体可见但未扣 → 异常
status = SeatbeltStatus.UNKNOWN
confidence = 0.5
else:
status = SeatbeltStatus.UNBUCKLED
confidence = 0.9
belt_visible = False

return SeatbeltInfo(
seat_row=0, # 需要外部设置
seat_position=0,
status=status,
confidence=confidence,
belt_visible=belt_visible
)

def _detect_belt_body(self, roi: np.ndarray) -> np.ndarray:
"""
检测安全带带体

Args:
roi: 乘员区域图像

Returns:
安全带掩码
"""
# 转换到 HSV 空间
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)

# 颜色检测(深色安全带)
mask = cv2.inRange(hsv, self.belt_color_lower, self.belt_color_upper)

# 形态学操作
kernel = np.ones((3, 3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)

return mask

def _analyze_belt_distribution(self, mask: np.ndarray) -> dict:
"""
分析安全带分布

Args:
mask: 安全带掩码

Returns:
各区域覆盖率字典
"""
h, w = mask.shape
total_pixels = h * w

distribution = {}
for zone_name, zone in self.BELT_ZONES.items():
y1 = int(zone['y'][0] * h)
y2 = int(zone['y'][1] * h)
x1 = int(zone['x'][0] * w)
x2 = int(zone['x'][1] * w)

zone_mask = mask[y1:y2, x1:x2]
zone_pixels = np.sum(zone_mask > 0)
zone_total = (y2 - y1) * (x2 - x1)

distribution[zone_name] = zone_pixels / zone_total if zone_total > 0 else 0

return distribution

2.4 儿童座椅检测

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
import numpy as np
import cv2
from typing import Tuple, Optional
from dataclasses import dataclass
from enum import Enum

class CRSType(Enum):
"""儿童座椅类型(Child Restraint System)"""
REAR_FACING = "rear_facing" # 后向
FORWARD_FACING = "forward_facing" # 前向
BOOSTER = "booster" # 增高座椅
UNKNOWN = "unknown"


@dataclass
class CRSInfo:
"""儿童座椅信息"""
seat_row: int
seat_position: int
crs_type: CRSType
confidence: float
bbox: Tuple[int, int, int, int]
child_present: bool # 是否有儿童


class CRSDetector:
"""
儿童座椅检测器

使用深度学习分类模型识别 CRS 类型
"""

# CRS 特征定义
CRS_FEATURES = {
CRSType.REAR_FACING: {
'orientation': 'backward',
'base_angle': '>120°',
'headrest': 'front'
},
CRSType.FORWARD_FACING: {
'orientation': 'forward',
'base_angle': '45-90°',
'headrest': 'back'
},
CRSType.BOOSTER: {
'orientation': 'forward',
'base_angle': '<30°',
'headrest': 'none'
}
}

def __init__(self, model_path: str = "crs_classifier.onnx"):
"""
初始化儿童座椅检测器

Args:
model_path: 分类模型路径
"""
# 加载模型(这里使用简化的特征方法)
self.model_path = model_path

def detect(self, frame: np.ndarray,
seat_zone: Tuple[int, int, int, int]) -> CRSInfo:
"""
检测儿童座椅

Args:
frame: BGR 图像
seat_zone: 座椅区域 (x1, y1, x2, y2)

Returns:
CRSInfo 对象
"""
x1, y1, x2, y2 = seat_zone

# 提取座椅区域
roi = frame[y1:y2, x1:x2]

# 检测是否有儿童座椅
has_crs = self._detect_crs_presence(roi)

if not has_crs:
return CRSInfo(
seat_row=0,
seat_position=0,
crs_type=CRSType.UNKNOWN,
confidence=0.0,
bbox=(0, 0, 0, 0),
child_present=False
)

# 分类 CRS 类型
crs_type, confidence = self._classify_crs_type(roi)

# 检测儿童存在
child_present = self._detect_child(roi, crs_type)

return CRSInfo(
seat_row=0,
seat_position=0,
crs_type=crs_type,
confidence=confidence,
bbox=seat_zone,
child_present=child_present
)

def _detect_crs_presence(self, roi: np.ndarray) -> bool:
"""检测是否有儿童座椅"""
# 简化实现:检测特定颜色/形状
# 实际需要训练好的检测模型

h, w = roi.shape[:2]
if h < 50 or w < 50:
return False

# 检测高对比度区域(儿童座椅特征)
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)

edge_ratio = np.sum(edges > 0) / (h * w)

return edge_ratio > 0.05

def _classify_crs_type(self, roi: np.ndarray) -> Tuple[CRSType, float]:
"""分类儿童座椅类型"""
# 简化实现:基于几何特征
# 实际需要训练好的分类模型

h, w = roi.shape[:2]

# 计算高宽比
aspect_ratio = h / w if w > 0 else 1.0

if aspect_ratio > 1.2:
# 高宽比较大 → 后向 CRS
return CRSType.REAR_FACING, 0.7
elif aspect_ratio > 0.9:
# 高宽比中等 → 前向 CRS
return CRSType.FORWARD_FACING, 0.7
else:
# 高宽比较小 → 增高座椅
return CRSType.BOOSTER, 0.6

def _detect_child(self, roi: np.ndarray, crs_type: CRSType) -> bool:
"""检测是否有儿童"""
# 简化实现:检测儿童头部
# 实际需要训练好的检测模型

# 使用 Haar 级联检测器
cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)

gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
faces = cascade.detectMultiScale(gray, 1.1, 3)

return len(faces) > 0

def get_airbag_status(self, crs_type: CRSType, child_present: bool) -> bool:
"""
获取气囊状态

Args:
crs_type: CRS 类型
child_present: 是否有儿童

Returns:
True 表示气囊 ON,False 表示 OFF
"""
if crs_type == CRSType.REAR_FACING and child_present:
return False # 后向 CRS 必须关闭气囊

# 其他情况保持开启
return True

2.5 异常姿态检测(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
import numpy as np
import cv2
import mediapipe as mp
from typing import Tuple, Optional, List
from dataclasses import dataclass
from enum import Enum

class OOPType(Enum):
"""异常姿态类型"""
FEET_DASHBOARD_INBOARD = "feet_dashboard_inboard"
FEET_DASHBOARD_CENTER = "feet_dashboard_center"
FEET_DASHBOARD_OUTBOARD = "feet_dashboard_outboard"
BODY_TOO_CLOSE = "body_too_close" # 头部距离仪表板 <20cm
NORMAL = "normal"


@dataclass
class OOPEvent:
"""异常姿态事件"""
timestamp: float
oop_type: OOPType
severity: int # 1=轻微, 2=严重
distance: float # 距离(cm)


class OOPDetector:
"""
异常姿态检测器(Out-of-Position Detection)

检测:
1. 脚踩仪表板(三个位置)
2. 上身前倾(头部距离仪表板 <20cm)
"""

# MediaPipe 姿态关键点索引
POSE_LANDMARKS = {
'nose': 0,
'left_shoulder': 11,
'right_shoulder': 12,
'left_hip': 23,
'right_hip': 24,
'left_knee': 25,
'right_knee': 26,
'left_ankle': 27,
'right_ankle': 28,
'left_heel': 29,
'right_heel': 30
}

def __init__(self,
dashboard_distance_cm: float = 60.0,
warning_threshold_cm: float = 20.0):
"""
初始化异常姿态检测器

Args:
dashboard_distance_cm: 仪表板距离摄像头的距离(cm)
warning_threshold_cm: 警告阈值(cm)
"""
self.dashboard_distance = dashboard_distance_cm
self.warning_threshold = warning_threshold_cm

# 初始化 MediaPipe Pose
self.mp_pose = mp.solutions.pose
self.pose = self.mp_pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
)

# 事件记录
self.events: List[OOPEvent] = []

def detect(self, frame: np.ndarray) -> Tuple[OOPType, Optional[OOPEvent]]:
"""
检测异常姿态

Args:
frame: BGR 图像

Returns:
oop_type: 异常姿态类型
event: 异常事件(如果检测到)
"""
h, w = frame.shape[:2]

# BGR 转 RGB
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

# 检测姿态
results = self.pose.process(rgb_frame)

if not results.pose_landmarks:
return OOPType.NORMAL, None

landmarks = results.pose_landmarks.landmark

# 检测脚踩仪表板
feet_oop = self._detect_feet_on_dashboard(landmarks, h, w)
if feet_oop != OOPType.NORMAL:
event = OOPEvent(
timestamp=cv2.getTickCount() / cv2.getTickFrequency(),
oop_type=feet_oop,
severity=1,
distance=0.0
)
self.events.append(event)
return feet_oop, event

# 检测身体前倾
body_oop, distance = self._detect_body_too_close(landmarks, h, w)
if body_oop != OOPType.NORMAL:
event = OOPEvent(
timestamp=cv2.getTickCount() / cv2.getTickFrequency(),
oop_type=body_oop,
severity=2,
distance=distance
)
self.events.append(event)
return body_oop, event

return OOPType.NORMAL, None

def _detect_feet_on_dashboard(self, landmarks, h: int, w: int) -> OOPType:
"""
检测脚踩仪表板

Args:
landmarks: 姿态关键点
h: 图像高度
w: 图像宽度

Returns:
OOPType 枚举值
"""
# 获取脚踝和脚跟位置
left_ankle = landmarks[self.POSE_LANDMARKS['left_ankle']]
right_ankle = landmarks[self.POSE_LANDMARKS['right_ankle']]

# 判断脚是否在仪表板区域(图像上半部分)
dashboard_y_threshold = 0.5 # 图像上半部分

if left_ankle.y < dashboard_y_threshold and left_ankle.visibility > 0.5:
# 左脚在仪表板
if left_ankle.x < 0.33:
return OOPType.FEET_DASHBOARD_INBOARD
elif left_ankle.x < 0.67:
return OOPType.FEET_DASHBOARD_CENTER
else:
return OOPType.FEET_DASHBOARD_OUTBOARD

if right_ankle.y < dashboard_y_threshold and right_ankle.visibility > 0.5:
# 右脚在仪表板
if right_ankle.x < 0.33:
return OOPType.FEET_DASHBOARD_INBOARD
elif right_ankle.x < 0.67:
return OOPType.FEET_DASHBOARD_CENTER
else:
return OOPType.FEET_DASHBOARD_OUTBOARD

return OOPType.NORMAL

def _detect_body_too_close(self, landmarks, h: int, w: int) -> Tuple[OOPType, float]:
"""
检测身体前倾

Args:
landmarks: 姿态关键点
h: 图像高度
w: 图像宽度

Returns:
(OOPType, distance_cm)
"""
# 获取鼻子位置(头部位置)
nose = landmarks[self.POSE_LANDMARKS['nose']]
left_shoulder = landmarks[self.POSE_LANDMARKS['left_shoulder']]
right_shoulder = landmarks[self.POSE_LANDMARKS['right_shoulder']]

# 计算头部到仪表板的距离(简化:使用 y 坐标)
# y 坐标越小,距离越近
head_y = nose.y
shoulder_y = (left_shoulder.y + right_shoulder.y) / 2

# 估算距离(需要标定)
# 假设 y=0.3 时距离为 60cm
estimated_distance = self.dashboard_distance * (0.3 / max(head_y, 0.1))

if estimated_distance < self.warning_threshold:
return OOPType.BODY_TOO_CLOSE, estimated_distance

return OOPType.NORMAL, estimated_distance

def close(self):
"""释放资源"""
self.pose.close()


# 测试代码
if __name__ == "__main__":
detector = OOPDetector()
cap = cv2.VideoCapture(0)

print("按 'q' 退出")
while True:
ret, frame = cap.read()
if not ret:
break

oop_type, event = detector.detect(frame)

if oop_type != OOPType.NORMAL:
# 显示警告
cv2.putText(frame, f"WARNING: {oop_type.value}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

if event:
cv2.putText(frame, f"Distance: {event.distance:.1f}cm", (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

cv2.imshow("OOP Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
detector.close()
cv2.destroyAllWindows()

三、Euro NCAP 测试场景详解

3.1 后排乘员检测测试场景

RO-01: 后排乘员存在检测

前置条件:

  • 后排座椅安装广角摄像头
  • 光照条件:白天/夜间均可
  • 摄像头帧率 ≥25fps

测试步骤:

  1. 所有后排座椅无人,记录检测结果
  2. 左后座椅坐成人,记录检测结果
  3. 右后座椅坐成人,记录检测结果
  4. 后排中间座椅坐儿童(CRS),记录检测结果

判定条件:

检测项 通过条件 失败条件
空座椅检测 正确识别为 EMPTY 误识别为 OCCUPIED
有人检测 正确识别为 OCCUPIED 漏检
CRS 检测 正确识别为 CHILD_SEAT 误识别为 OCCUPIED

RO-02: 安全带误用检测

前置条件:

  • 后排座椅有乘员
  • 安全带扣传感器正常工作

测试步骤:

  1. 正确佩戴安全带,记录检测结果
  2. 解开安全带,记录检测结果
  3. 仅扣安全带扣(带体放在背后),记录检测结果
  4. 仅腰部安全带(肩带放在背后),记录检测结果

判定条件:

检测项 通过条件 失败条件
正确佩戴 BUCKLED_CORRECT 误判
未系安全带 UNBUCKLED + 警告 未警告
仅扣安全带扣 BUCKLE_ONLY + 警告 未检测/未警告
仅腰部安全带 LAP_ONLY + 警告 未检测/未警告

3.2 异常姿态检测测试场景

OOP-01: 脚踩仪表板检测

前置条件:

  • 前排乘客座椅有人
  • 摄像头覆盖仪表板区域

测试步骤:

  1. 乘客正常坐姿,记录检测结果
  2. 乘客将脚放在仪表板左侧(内侧),记录检测结果
  3. 乘客将脚放在仪表板中间,记录检测结果
  4. 乘客将脚放在仪表板右侧(外侧),记录检测结果

判定条件:

检测项 通过条件 失败条件
正常坐姿 NORMAL 误报
脚踩仪表板 正确分类位置 + 警告 未检测/位置错误

预期输出:

1
2
3
4
5
6
7
[00:00] INFO: 开始测试 OOP-01
[00:10] INFO: 正常坐姿,检测结果: NORMAL
[00:20] WARN: 检测到脚踩仪表板(内侧)
[00:20] WARN: 触发警告(异常姿态)
[00:30] WARN: 检测到脚踩仪表板(中间)
[00:40] WARN: 检测到脚踩仪表板(外侧)
[00:50] PASS: OOP-01 测试通过,检测率: 3/3

OOP-02: 身体前倾检测

前置条件:

  • 前排乘客座椅有人
  • 已标定摄像头距离

测试步骤:

  1. 乘客正常坐姿,记录检测结果
  2. 乘客前倾,头部距离仪表板 25cm,记录检测结果
  3. 乘客前倾,头部距离仪表板 15cm,记录检测结果

判定条件:

检测项 通过条件 失败条件
正常坐姿 NORMAL 误报
距离 25cm NORMAL 或轻微警告 严重警告
距离 <20cm BODY_TOO_CLOSE + 警告 未检测

四、硬件选型指南

4.1 后排摄像头配置

参数 基础要求 推荐配置 高端配置
分辨率 1280×720 1920×1080 1920×1080
帧率 25fps 30fps 60fps
视场角 120° 140° 160°
类型 RGB RGB RGB-D(深度)
安装位置 车顶后部 车顶后部 车顶后部 + B柱

4.2 深度传感器选型

传感器 技术 测距范围 精度 适用场景
IWR6843AOP 60GHz mmWave 0.2-3m ±2cm CPD + 乘员检测
ToF Camera 飞行时间 0.5-5m ±1cm OOP + 体型分类
Stereo Camera 双目立体视觉 0.5-10m ±3cm 全场景

4.3 处理器平台

平台 OMS 性能 功耗 适用场景
QCS8255 <30ms 8W 高端车型
TDA4VM <40ms 7W 中端车型
RK3588 <35ms 8W 性价比方案

五、IMS 开发优先级

优先级 模块 功能 精度要求
P0 后排乘员检测 存在/空座分类 召回率 >95%
P0 安全带状态检测 正确佩戴/误用分类 准确率 >85%
P0 CRS 类型识别 后向/前向/增高座椅 准确率 >80%
P1 异常姿态检测 脚踩仪表板/身体前倾 召回率 >90%
P1 乘员体型分类 5%/50%/95% 百分位 准确率 >80%
P2 气囊自适应 根据乘员类型控制 响应时间 <10s

六、参考资料

6.1 官方文档

6.2 技术博客


发布日期: 2026-04-16
标签: Euro NCAP, OMS, 后排监控, 安全带误用, 儿童座椅, 气囊自适应, OOP
更新记录: v1.0 - 初始版本,完整实现后排乘员监控与安全带误用检测


Euro NCAP 2026 后排乘员监控深度解析:安全带误用检测、气囊自适应与 OOP 完整实现指南
https://dapalm.com/2026/04/16/2026-04-16-euro-ncap-2026-rear-occupant-monitoring-seatbelt-misuse/
作者
Mars
发布于
2026年4月16日
许可协议