乘员姿态估计与OOP检测:从2D关键点到3D座舱建模

背景:Euro NCAP OOP检测要求

Euro NCAP 2026引入异常姿态(OOP)检测

OOP类型 定义 安全风险
躺卧 乘员平躺座椅上 安全气囊失效
前倾 头部过度前倾 气囊冲击伤害
侧靠 身体侧倚门板 侧气囊风险
脚放仪表台 腿部异常放置 碰撞损伤

检测难点:

  • 3D姿态需要深度信息
  • 座椅角度变化影响判断
  • 遮挡(安全带/手臂)

技术路线

1. 2D关键点检测

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
"""
2D人体关键点检测
"""
class HumanKeypointDetector:
"""
人体关键点检测器

模型:MediaPipe Pose / BlazePose
输出:33个3D关键点(相对坐标系)
"""

def __init__(self, model_type='mediapipe'):
if model_type == 'mediapipe':
import mediapipe as mp
self.pose = mp.solutions.pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5
)

# COCO关键点定义
self.keypoint_names = [
'nose', 'left_eye_inner', 'left_eye', 'left_eye_outer',
'right_eye_inner', 'right_eye', 'right_eye_outer',
'left_ear', 'right_ear', 'mouth_left', 'mouth_right',
'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
'left_wrist', 'right_wrist', 'left_pinky', 'right_pinky',
'left_index', 'right_index', 'left_thumb', 'right_thumb',
'left_hip', 'right_hip', 'left_knee', 'right_knee',
'left_ankle', 'right_ankle', 'left_heel', 'right_heel',
'left_foot_index', 'right_foot_index'
]

def detect(self, image):
"""
检测关键点

Args:
image: RGB图像 (H, W, 3)

Returns:
keypoints: (33, 4) [x, y, z, visibility]
"""
results = self.pose.process(image)

if results.pose_landmarks:
landmarks = results.pose_landmarks.landmark

keypoints = []
for lm in landmarks:
keypoints.append([lm.x, lm.y, lm.z, lm.visibility])

return np.array(keypoints)

return None

def visualize_keypoints(self, image, keypoints):
"""可视化关键点"""
h, w = image.shape[:2]

for i, kp in enumerate(keypoints):
if kp[3] > 0.5: # visibility
x, y = int(kp[0] * w), int(kp[1] * h)
cv2.circle(image, (x, y), 3, (0, 255, 0), -1)
cv2.putText(image, str(i), (x+5, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 0, 0), 1)

return image

2. 3D姿态估计

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
"""
单目3D姿态估计
"""
class Monocular3DPoseEstimator:
"""
单目3D姿态估计

方法:
1. 2D关键点检测
2. 相机内参投影
3. 深度估计(单目深度网络)
4. 3D重建
"""

def __init__(self, camera_intrinsics):
self.K = camera_intrinsics # 3x3内参矩阵
self.keypoint_detector = HumanKeypointDetector()
self.depth_estimator = MonocularDepthEstimator()

def estimate_3d_pose(self, image):
"""
估计3D人体姿态

Returns:
pose_3d: (33, 3) 关键点3D坐标(米)
"""
# 1. 检测2D关键点
keypoints_2d = self.keypoint_detector.detect(image)

if keypoints_2d is None:
return None

# 2. 估计深度
depth_map = self.depth_estimator.estimate(image)

# 3. 反投影到3D
pose_3d = []

for kp in keypoints_2d:
x, y, z, vis = kp

if vis > 0.5:
# 获取该点深度
h, w = depth_map.shape
px, py = int(x * w), int(y * h)
depth = depth_map[py, px]

# 反投影
X = (x - self.K[0, 2]) * depth / self.K[0, 0]
Y = (y - self.K[1, 2]) * depth / self.K[1, 1]
Z = depth

pose_3d.append([X, Y, Z])
else:
pose_3d.append([0, 0, 0])

return np.array(pose_3d)


class MonocularDepthEstimator(nn.Module):
"""
单目深度估计网络

模型:MiDaS / DPT
"""

def __init__(self, model_type='DPT_Large'):
super().__init__()

# 加载预训练模型
self.model = torch.hub.load('intel-isl/MiDaS', model_type)
self.transform = torch.hub.load('intel-isl/MiDaS', 'transforms')

def estimate(self, image):
"""
估计深度图

Args:
image: RGB图像 (H, W, 3)

Returns:
depth_map: (H, W) 相对深度
"""
# 预处理
input_tensor = self.transform(image).unsqueeze(0)

# 推理
with torch.no_grad():
depth = self.model(input_tensor)

# 后处理
depth_map = torch.nn.functional.interpolate(
depth.unsqueeze(1),
size=image.shape[:2],
mode='bilinear',
align_corners=False
).squeeze()

return depth_map.cpu().numpy()

3. 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
"""
OOP异常姿态分类
"""
class OOPClassifier:
"""
异常姿态分类器

类别:
0: 正常坐姿
1: 躺卧(OOP-LYING)
2: 前倾(OOP-FORWARD)
3: 侧靠(OOP-LEANING)
4: 脚放仪表台(OOP-DASHBOARD)
"""

def __init__(self):
# 定义关键角度阈值
self.angle_thresholds = {
'torso_vertical': 75, # 上身垂直角度阈值(度)
'head_forward': 30, # 头部前倾角度阈值
'shoulder_tilt': 20 # 肩膀倾斜阈值
}

# 关键点索引
self.KEYPOINTS = {
'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
}

def classify(self, pose_3d):
"""
分类姿态

Args:
pose_3d: (33, 3) 3D关键点

Returns:
classification: {
'oop_type': str,
'confidence': float,
'angles': dict
}
"""
# 计算关键角度
angles = self._calculate_key_angles(pose_3d)

# 规则判断
oop_type = 'NORMAL'
confidence = 0.0

# 躺卧检测:上身接近水平
if angles['torso_vertical'] < self.angle_thresholds['torso_vertical']:
oop_type = 'OOP_LYING'
confidence = 1.0 - angles['torso_vertical'] / 90

# 前倾检测:头部过度前倾
elif angles['head_forward'] > self.angle_thresholds['head_forward']:
oop_type = 'OOP_FORWARD'
confidence = angles['head_forward'] / 60

# 侧靠检测:肩膀倾斜
elif angles['shoulder_tilt'] > self.angle_thresholds['shoulder_tilt']:
oop_type = 'OOP_LEANING'
confidence = angles['shoulder_tilt'] / 45

return {
'oop_type': oop_type,
'confidence': confidence,
'angles': angles
}

def _calculate_key_angles(self, pose_3d):
"""计算关键角度"""
angles = {}

# 上身垂直角度(髋到肩连线与垂直方向夹角)
hip_center = (pose_3d[23] + pose_3d[24]) / 2
shoulder_center = (pose_3d[11] + pose_3d[12]) / 2

torso_vector = shoulder_center - hip_center
vertical = np.array([0, -1, 0])

angles['torso_vertical'] = self._angle_between(torso_vector, vertical)

# 头部前倾角度(鼻到肩连线与垂直方向夹角)
nose = pose_3d[0]
head_vector = nose - shoulder_center
angles['head_forward'] = self._angle_between(head_vector, vertical)

# 肩膀倾斜角度(左右肩高度差)
left_shoulder = pose_3d[11]
right_shoulder = pose_3d[12]
shoulder_diff = np.abs(left_shoulder[1] - right_shoulder[1])
shoulder_width = np.linalg.norm(left_shoulder - right_shoulder)
angles['shoulder_tilt'] = np.arcsin(shoulder_diff / shoulder_width) * 180 / np.pi

return angles

def _angle_between(self, v1, v2):
"""计算两向量夹角(度)"""
cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-10)
return np.arccos(np.clip(cos_angle, -1, 1)) * 180 / np.pi


class MLOOPClassifier(nn.Module):
"""
机器学习OOP分类器

端到端学习,精度更高
"""

def __init__(self):
super().__init__()

# 姿态编码器
self.pose_encoder = nn.Sequential(
nn.Linear(33 * 3, 512),
nn.ReLU(),
nn.Linear(512, 256),
nn.ReLU()
)

# 分类头
self.classifier = nn.Sequential(
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 5) # 5类
)

def forward(self, pose_3d):
"""
Args:
pose_3d: (B, 33, 3)

Returns:
logits: (B, 5)
"""
# Flatten
x = pose_3d.view(pose_3d.shape[0], -1)

# 编码
features = self.pose_encoder(x)

# 分类
logits = self.classifier(features)

return logits

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
"""
座椅角度补偿
"""
class SeatAngleCompensator:
"""
座椅角度补偿器

原因:
- 座椅靠背角度变化影响姿态判断
- 需要补偿座椅角度才能准确判断OOP
"""

def __init__(self):
self.seat_angle = 0 # 度(相对垂直)

def set_seat_angle(self, angle):
"""设置座椅角度(从CAN总线读取)"""
self.seat_angle = angle

def compensate(self, pose_3d):
"""
补偿座椅角度

将姿态转换到座椅坐标系
"""
# 座椅旋转矩阵
R_seat = self._rotation_matrix_x(self.seat_angle * np.pi / 180)

# 旋转姿态
pose_compensated = pose_3d @ R_seat.T

return pose_compensated

def _rotation_matrix_x(self, angle):
"""X轴旋转矩阵"""
return np.array([
[1, 0, 0],
[0, np.cos(angle), -np.sin(angle)],
[0, np.sin(angle), np.cos(angle)]
])

Euro NCAP对接

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
def encap_oop_test(oop_system, test_cases):
"""
Euro NCAP OOP检测测试

测试场景:
1. 正常坐姿(不应误报)
2. 躺卧(应检测到)
3. 前倾(应检测到)
4. 侧靠(应检测到)
5. 儿童座椅(特殊处理)
"""
results = []

for case in test_cases:
# 设置场景
pose = case['pose']
seat_angle = case.get('seat_angle', 25)

# 补偿座椅角度
oop_system.set_seat_angle(seat_angle)

# 检测
result = oop_system.classify(pose)

# 评估
expected = case['expected_oop_type']
detected = result['oop_type']

results.append({
'case': case['name'],
'expected': expected,
'detected': detected,
'pass': expected == detected,
'confidence': result['confidence']
})

# 统计
accuracy = sum(r['pass'] for r in results) / len(results)

return {
'accuracy': accuracy,
'encap_pass': accuracy >= 0.95
}

技术亮点总结

方面 贡献
3D姿态 单目深度估计+3D重建
座椅补偿 基于CAN数据的座椅角度补偿
规则+ML 结合规则判断和端到端学习
实时性 30fps推理速度

参考文献

  1. MediaPipe Pose: “BlazePose”, 2020
  2. MiDaS: “Towards Robust Monocular Depth Estimation”, 2023
  3. Euro NCAP: “Occupant Monitoring Protocol v0.9”, 2024

开发优先级: 🟡 中(Euro NCAP 2026新增)
技术成熟度: TRL 5(原型验证)
部署难度: 中等(需深度传感器或双目相机)
量产时间线: 2027-2028年


乘员姿态估计与OOP检测:从2D关键点到3D座舱建模
https://dapalm.com/2026/07/23/2026-07-23-occupant-posture-oop-detection/
作者
Mars
发布于
2026年7月23日
许可协议