车内乘员 3D 姿态估计:深度+红外图像三阶段微调方案(Sensors 2024 论文解读)

发布时间: 2026-09-05
分类: 论文解读 / OOP异常姿态 / 3D姿态
标签: 3D posture, depth image, infrared, LiDAR, vehicle occupant, OOP, Euro NCAP


论文信息

项目 内容
标题 Three-Dimensional Posture Estimation of Vehicle Occupants Using Depth and Infrared Images
作者 Tambwekar A., Park B.-K.D., Kusari A., Sun W.
期刊 Sensors, 24(17), 5530
DOI 10.3390/s24175530
发表日期 2024-08-27
关键词 posture estimation, depth-sensing, LiDAR, vehicular occupant safety

1. 核心创新

1.1 研究动机

Euro NCAP 2026 新增 OOP(Out-of-Position)异常姿态检测要求,需要检测乘员是否处于非标准坐姿(前倾、侧倾、脚搭仪表台等),以适配安全气囊部署策略。

挑战 RGB 方案 深度+IR 方案
3D 信息 ❌ 缺失 ✅ 直接获取
光照鲁棒 ❌ 敏感 ✅ 不受影响
遮挡处理 ❌ 差 ✅ 穿透遮挡
隐私保护 ❌ 拍摄人脸 ✅ 无身份信息
标注成本 中 🔴 极高(5-10分钟/样本)

1.2 三大贡献

  1. 首个仅使用深度+IR 图像的车内乘员 3D 姿态估计方法
  2. 三阶段微调训练策略:仿真→域适配→真实标注,仅需 <100 人工标注样本
  3. 中位误差 <10cm:覆盖 15 个关键关节点

2. 方法详解

2.1 问题定义

$$Z = f_\theta(Y, s, c_1, c_2)$$

  • $Z$:3D 姿态矩阵 (15×3),15 个关节的 3D 坐标
  • $Y$:深度+IR 图像
  • $s$:乘员体型参数
  • $c_1, c_2$:两个摄像头参数
  • $f_\theta$:神经网络

2.2 15 个关键关节

部位 关节 数量
躯干 骨盆、腹部、胸部、颈部、头部 5
左臂 左肩、左肘、左腕 3
右臂 右肩、右肘、右腕 3
左腿 左髋、左膝 2
右腿 右髋、右膝 2
合计 15

注:下肢末端(踝/脚)因被座椅遮挡而省略

2.3 三阶段微调训练策略

阶段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
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
import numpy as np
import torch
import torch.nn as nn
from typing import Tuple

class SimulationDataGenerator:
"""
阶段1:仿真数据生成

使用 SMPL-X 统计人体模型生成合成深度+IR 图像

流程:
1. 采样姿态参数 Z (15关节 × 3D)
2. 采样体型参数 s (身高/体重/比例)
3. 通过 SMPL-X 生成人体网格
4. 渲染深度图和 IR 图
"""

def __init__(self, num_joints: int = 15):
self.num_joints = num_joints
# SMPL-X 关节层级
self.kinematic_tree = {
'pelvis': 0,
'abdomen': 1, 'thorax': 2, 'neck': 3, 'head': 4,
'left_hip': 5, 'left_knee': 6,
'right_hip': 7, 'right_knee': 8,
'left_shoulder': 9, 'left_elbow': 10, 'left_wrist': 11,
'right_shoulder': 12, 'right_elbow': 13, 'right_wrist': 14
}

# 车内典型姿态范围
self.posture_ranges = {
'normal_driving': {'neck_pitch': (-10, 10), 'torso_pitch': (-5, 15)},
'leaning_forward': {'neck_pitch': (20, 45), 'torso_pitch': (20, 40)},
'leaning_left': {'neck_yaw': (-30, -15), 'torso_roll': (-20, -10)},
'leaning_right': {'neck_yaw': (15, 30), 'torso_roll': (10, 20)},
'feet_up': {'hip_flexion': (60, 90), 'knee_flexion': (-30, 0)},
'sleeping': {'neck_pitch': (30, 60), 'torso_roll': (-15, 15)}
}

def generate_sample(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
生成一个仿真样本

Returns:
depth_image: (H, W) 深度图
ir_image: (H, W) 红外图
posture_3d: (15, 3) 3D 关节坐标
"""
# 随机选择姿态类型
posture_type = np.random.choice(list(self.posture_ranges.keys()))
ranges = self.posture_ranges[posture_type]

# 采样关节角度
angles = {}
for joint, (low, high) in ranges.items():
angles[joint] = np.random.uniform(low, high)

# 采样体型参数
height = np.random.uniform(1.5, 1.9) # 米
weight = np.random.uniform(50, 100) # kg

# 生成 3D 关节坐标(简化版前向运动学)
posture_3d = self._forward_kinematics(angles, height)

# 渲染深度图和 IR 图(模拟)
H, W = 480, 640
depth_image = self._render_depth(posture_3d, H, W)
ir_image = self._render_ir(posture_3d, H, W)

return depth_image, ir_image, posture_3d

def _forward_kinematics(self, angles: dict, height: float) -> np.ndarray:
"""简化版前向运动学"""
posture = np.zeros((15, 3))

# 骨盆位置(原点)
posture[0] = [0, 0, 0.4 * height]

# 躯干向上
posture[1] = posture[0] + [0, 0, 0.15 * height]
posture[2] = posture[1] + [0, 0, 0.15 * height]

# 颈部
neck_pitch = np.radians(angles.get('neck_pitch', 0))
posture[3] = posture[2] + [0, 0.05 * height * np.sin(neck_pitch),
0.08 * height * np.cos(neck_pitch)]

# 头部
posture[4] = posture[3] + [0, 0.05 * height * np.sin(neck_pitch),
0.12 * height * np.cos(neck_pitch)]

# 四肢(简化)
posture[5] = posture[0] + [0.08, 0, -0.05] # 左髋
posture[6] = posture[5] + [0, 0, -0.2] # 左膝
posture[7] = posture[0] + [-0.08, 0, -0.05] # 右髋
posture[8] = posture[7] + [0, 0, -0.2] # 右膝

posture[9] = posture[2] + [0.15, 0, 0] # 左肩
posture[10] = posture[9] + [0.1, 0, -0.1] # 左肘
posture[11] = posture[10] + [0.1, 0, -0.1] # 左腕
posture[12] = posture[2] + [-0.15, 0, 0] # 右肩
posture[13] = posture[12] + [-0.1, 0, -0.1]
posture[14] = posture[13] + [-0.1, 0, -0.1]

return posture

def _render_depth(self, posture: np.ndarray, H: int, W: int) -> np.ndarray:
"""模拟深度图渲染"""
depth = np.full((H, W), 5.0) # 背景距离 5m

for joint in posture:
x = int(W/2 + joint[0] * 100)
y = int(H/2 - joint[2] * 100)
z = joint[1] + 1.0 # 前方 1m

if 0 <= x < W and 0 <= y < H:
cv2_circle(depth, (x, y), 15, z, -1)

return depth

def _render_ir(self, posture: np.ndarray, H: int, W: int) -> np.ndarray:
"""模拟 IR 图渲染"""
ir = np.zeros((H, W), dtype=np.float32)

for joint in posture:
x = int(W/2 + joint[0] * 100)
y = int(H/2 - joint[2] * 100)

if 0 <= x < W and 0 <= y < H:
ir[y-10:y+10, x-10:x+10] = 0.8

return ir


def cv2_circle(img, center, radius, color, thickness):
"""简化版圆形绘制"""
H, W = img.shape[:2]
cx, cy = center
for y in range(max(0, cy-radius), min(H, cy+radius)):
for x in range(max(0, cx-radius), min(W, cx+radius)):
if (x-cx)**2 + (y-cy)**2 <= radius**2:
img[y, x] = color

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
class OccupantPostureNet(nn.Module):
"""
车内乘员 3D 姿态估计网络

输入:深度图 + IR 图
输出:15 个关节的 3D 坐标

架构:双流编码器 + 融合解码器
"""

def __init__(
self,
num_joints: int = 15,
input_size: int = 480,
hidden_dim: int = 256
):
super().__init__()
self.num_joints = num_joints

# 深度图编码器
self.depth_encoder = nn.Sequential(
nn.Conv2d(1, 32, 3, stride=2, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1))
)

# IR 图编码器
self.ir_encoder = nn.Sequential(
nn.Conv2d(1, 32, 3, stride=2, padding=1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, 3, stride=2, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1))
)

# 融合层
self.fusion = nn.Sequential(
nn.Linear(256, hidden_dim),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(0.3)
)

# 关节坐标回归器
self.regressor = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, num_joints * 3)
)

def forward(
self,
depth: torch.Tensor, # (B, 1, H, W)
ir: torch.Tensor # (B, 1, H, W)
) -> torch.Tensor:
"""
Returns:
posture_3d: (B, 15, 3) 3D 关节坐标
"""
# 双流编码
depth_feat = self.depth_encoder(depth).flatten(1) # (B, 128)
ir_feat = self.ir_encoder(ir).flatten(1) # (B, 128)

# 融合
fused = torch.cat([depth_feat, ir_feat], dim=-1) # (B, 256)
fused = self.fusion(fused) # (B, hidden_dim)

# 回归
posture = self.regressor(fused) # (B, 15*3)
posture = posture.reshape(-1, self.num_joints, 3) # (B, 15, 3)

return posture


if __name__ == "__main__":
# 模型初始化
model = OccupantPostureNet(num_joints=15)

total_params = sum(p.numel() for p in model.parameters())
print(f"模型参数量: {total_params:,}")

# 模拟输入
depth = torch.randn(4, 1, 480, 640)
ir = torch.randn(4, 1, 480, 640)

with torch.no_grad():
posture = model(depth, ir)

print(f"输入: depth={depth.shape}, ir={ir.shape}")
print(f"输出: posture={posture.shape}")

# 仿真数据生成
generator = SimulationDataGenerator()
for i in range(3):
d, ir_img, pose = generator.generate_sample()
print(f"\n样本 {i+1}:")
print(f" 深度图: {d.shape}")
print(f" IR图: {ir_img.shape}")
print(f" 姿态: {pose.shape}")
print(f" 头部位置: ({pose[4,0]:.2f}, {pose[4,1]:.2f}, {pose[4,2]:.2f})")

print("\n=== 论文报告结果 ===")
print("中位误差: <10cm (所有关节)")
print("标注样本: <100 个")
print("关节点数: 15")
print("输入: 深度+IR (双 Kinect V2)")

3. 三阶段训练详解

3.1 阶段对比

阶段 数据来源 样本量 标注方式 作用
Stage 1 SMPL-X 仿真 10,000+ 自动 预训练
Stage 2 OptiTrack 动捕 ~1,000 半自动 域适配
Stage 3 人工标注 <100 精确 最终微调

3.2 域适配关键

从仿真到真实数据的域差距(domain gap)是核心挑战:

差距来源 仿真 真实 缓解策略
传感器特性 理想渲染 Kinect V2 噪声 加入传感器噪声模型
人体形状 SMPL-X 参数化 真实人体 增加体型多样性
车内环境 简化几何 复杂反射 加入车内 3D 模型
遮挡 无遮挡 座椅/方向盘遮挡 模拟遮挡

4. Euro NCAP OOP 检测要求

4.1 OOP 场景定义

OOP 场景 描述 安全风险
前倾 躯干前倾 >30° 气囊伤害
侧倾 躯干侧倾 >20° 侧面气囊失效
脚搭仪表台 腿部高于标准位置 气囊冲击腿
反向坐 面向后方 气囊完全无效
躺卧 近水平姿态 安全带失效
跪姿 膝盖着地 下肢伤害

4.2 检测要求

指标 要求 论文方案
关节误差 <10cm ✅ 中位 <10cm
检测时间 ≤2秒 取决于硬件
覆盖关节 ≥12个 ✅ 15个
3D 信息 必须 ✅ 直接 3D
隐私 不拍人脸 ✅ 深度+IR

5. 对 IMS 开发的启示

5.1 技术路线选择

方案 传感器 优势 劣势 推荐度
深度+IR Kinect/ToF 3D直接、隐私 成本中、分辨率有限 ⭐⭐⭐⭐
RGB多视角 多摄像头 高分辨率、成熟 隐私问题、3D需推断 ⭐⭐⭐
毫米波雷达 60GHz 穿透、非接触 分辨率不足 ⭐⭐
融合方案 深度+IR+RGB 最全面 成本高 ⭐⭐⭐⭐⭐

5.2 三阶段训练策略的通用价值

这个三阶段训练策略不仅适用于 OOP 检测,可推广到:

  • CPD 儿童检测:仿真儿童体型→域适配→真实标注
  • 疲劳检测:仿真面部表情→域适配→真实标注
  • 认知分心:仿真眼动模式→域适配→真实标注

5.3 部署建议

建议 优先级 理由
评估 ToF 深度摄像头方案 🔴 高 隐私+3D双重优势
建立仿真数据管线 🔴 高 减少标注成本
复用三阶段微调策略 🟡 中 已验证有效
对标 Euro NCAP OOP 场景 🔴 高 法规驱动

6. 参考资源


OOP 异常姿态检测是 Euro NCAP 2026 的新增要求,深度+IR 方案以隐私和 3D 双重优势成为有力竞争者。三阶段微调策略解决了标注成本难题,可推广到 IMS 多个功能模块。