安全带误用检测:DMS/OMS鲁棒方案

论文: Robust Seatbelt Detection and Usage Recognition for Driver Monitoring Systems
作者: Feng Hu et al.
会议: AAAI 2022 Workshop on Trustworthy Autonomous Systems Engineering
链接: https://arxiv.org/abs/2203.00810


Euro NCAP 2026 安全带要求

新增检测场景

Euro NCAP 2026 要求识别安全带误用场景:

误用类型 描述 危险程度
未系安全带 完全未系 🔴 极高
安全带在身后 座椅已扣好,但人未系 🔴 高
安全带在腋下 绕过肩部从腋下穿过 🟡 中
安全带松弛 未拉紧,存在间隙 🟡 中
错误位置 压在颈部/腹部而非骨盆 🟡 中

传统系统缺陷

缺陷 描述 风险
“消音器”欺诈 插入安全带卡扣但不系在身上 完全失效
无颜色信息 红外摄像头无法区分颜色 检测困难
鱼眼畸变 大FOV镜头导致严重变形 定位误差
低对比度 安全带与背景颜色相近 漏检

方法详解

三阶段框架

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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Tuple, List, Dict

class SeatbeltDetector(nn.Module):
"""
安全带检测与误用识别框架

三阶段:
1. Local Predictor - 局部关键点预测
2. Global Assembler - 全局形状组装
3. Shape Modeling - 形状合理性建模
"""

def __init__(self, config: dict):
super().__init__()

# 局部预测器(关键点检测)
self.local_predictor = LocalPredictor(
backbone='resnet18',
num_keypoints=6 # 安全带关键点
)

# 全局组装器(曲线拟合)
self.global_assembler = GlobalAssembler(
num_control_points=8
)

# 形状建模器(误用分类)
self.shape_classifier = ShapeClassifier(
num_classes=5 # 正确/未系/身后/腋下/松弛
)

def forward(self, image: torch.Tensor) -> Dict:
"""
Args:
image: (B, 3, H, W) 输入图像(可见光或红外)

Returns:
result: {
'keypoints': 安全带关键点,
'belt_mask': 安全带分割,
'usage_type': 使用状态分类,
'confidence': 检测置信度
}
"""
# 1. 局部关键点预测
keypoints, heatmaps = self.local_predictor(image)

# 2. 全局形状组装
belt_curve, control_points = self.global_assembler(keypoints, heatmaps)

# 3. 形状建模与分类
usage_type, confidence = self.shape_classifier(belt_curve, keypoints)

return {
'keypoints': keypoints,
'belt_curve': belt_curve,
'usage_type': usage_type,
'confidence': confidence
}


class LocalPredictor(nn.Module):
"""
局部关键点预测器

检测安全带的6个关键点:
1. 左肩点
2. 右肩点(跨肩)
3. 左腰点
4. 右腰点(扣点)
5. 中间点(胸骨)
6. 斜跨点
"""

def __init__(self, backbone: str = 'resnet18', num_keypoints: int = 6):
super().__init__()

# 骨干网络
if backbone == 'resnet18':
self.backbone = nn.Sequential(
nn.Conv2d(3, 64, 7, 2, 3),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, 1),

self._make_res_block(64, 64),
self._make_res_block(64, 128, stride=2),
self._make_res_block(128, 256, stride=2),
self._make_res_block(256, 512, stride=2),
)
self.feature_dim = 512
else:
raise ValueError(f"Unsupported backbone: {backbone}")

# 关键点热图头
self.keypoint_head = nn.Sequential(
nn.Conv2d(self.feature_dim, 256, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 128, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(128, num_keypoints, 1),
)

# 偏移量头(精细化定位)
self.offset_head = nn.Conv2d(self.feature_dim, num_keypoints * 2, 1)

self.num_keypoints = num_keypoints

def forward(self, image: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
image: (B, 3, H, W)

Returns:
keypoints: (B, K, 2) 关键点坐标
heatmaps: (B, K, H', W') 热图
"""
# 提取特征
feat = self.backbone(image)

# 预测热图
heatmaps = self.keypoint_head(feat) # (B, K, H', W')

# 预测偏移
offsets = self.offset_head(feat) # (B, K*2, H', W')

# 解码关键点
keypoints = self._decode_keypoints(heatmaps, offsets)

return keypoints, heatmaps

def _decode_keypoints(
self,
heatmaps: torch.Tensor,
offsets: torch.Tensor
) -> torch.Tensor:
"""
从热图解码关键点坐标

Args:
heatmaps: (B, K, H', W')
offsets: (B, K*2, H', W')

Returns:
keypoints: (B, K, 2)
"""
B, K, H, W = heatmaps.shape

# 找最大值位置
heatmaps_flat = heatmaps.view(B, K, -1)
max_indices = torch.argmax(heatmaps_flat, dim=2) # (B, K)

# 转换为坐标
xs = (max_indices % W).float()
ys = (max_indices // W).float()

# 加偏移量精细化
offsets = offsets.view(B, K, 2, H, W)

keypoints = torch.zeros(B, K, 2, device=heatmaps.device)
for k in range(K):
for b in range(B):
x = xs[b, k].long()
y = ys[b, k].long()

keypoints[b, k, 0] = xs[b, k] + offsets[b, k, 0, y, x]
keypoints[b, k, 1] = ys[b, k] + offsets[b, k, 1, y, x]

# 归一化到原图坐标
keypoints[:, :, 0] = keypoints[:, :, 0] * (image.shape[3] / W)
keypoints[:, :, 1] = keypoints[:, :, 1] * (image.shape[2] / H)

return keypoints

def _make_res_block(self, in_ch, out_ch, stride=1):
"""构建残差块"""
return nn.Sequential(
nn.Conv2d(in_ch, out_ch, 3, stride, 1),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Conv2d(out_ch, out_ch, 3, 1, 1),
nn.BatchNorm2d(out_ch),
)


class GlobalAssembler(nn.Module):
"""
全局组装器

将离散关键点组装成连续安全带曲线
"""

def __init__(self, num_control_points: int = 8):
super().__init__()

self.num_control_points = num_control_points

# 控制点预测网络
self.control_net = nn.Sequential(
nn.Linear(12, 64), # 6 keypoints * 2
nn.ReLU(inplace=True),
nn.Linear(64, 128),
nn.ReLU(inplace=True),
nn.Linear(128, num_control_points * 2),
)

def forward(
self,
keypoints: torch.Tensor,
heatmaps: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
keypoints: (B, K, 2) 关键点
heatmaps: (B, K, H', W') 热图

Returns:
belt_curve: (B, 100, 2) 安全带曲线点
control_points: (B, C, 2) 控制点
"""
# 预测控制点
keypoints_flat = keypoints.view(keypoints.shape[0], -1)
control_points = self.control_net(keypoints_flat)
control_points = control_points.view(-1, self.num_control_points, 2)

# 生成B样条曲线
belt_curve = self._generate_bspline(control_points, num_points=100)

return belt_curve, control_points

def _generate_bspline(
self,
control_points: torch.Tensor,
num_points: int = 100
) -> torch.Tensor:
"""
生成B样条曲线

Args:
control_points: (B, C, 2)
num_points: 曲线点数

Returns:
curve: (B, num_points, 2)
"""
B, C, _ = control_points.shape

# 参数t均匀采样
t = torch.linspace(0, 1, num_points, device=control_points.device)

# 简化:使用Catmull-Rom样条
curve = torch.zeros(B, num_points, 2, device=control_points.device)

for i in range(num_points):
# 找到对应的控制点段
seg_idx = int(t[i] * (C - 1))
seg_idx = min(seg_idx, C - 2)

# 线性插值(简化版)
alpha = (t[i] * (C - 1)) - seg_idx

curve[:, i, 0] = (
control_points[:, seg_idx, 0] * (1 - alpha) +
control_points[:, seg_idx + 1, 0] * alpha
)
curve[:, i, 1] = (
control_points[:, seg_idx, 1] * (1 - alpha) +
control_points[:, seg_idx + 1, 1] * alpha
)

return curve


class ShapeClassifier(nn.Module):
"""
形状分类器

基于曲线形状判断安全带使用状态
"""

def __init__(self, num_classes: int = 5):
super().__init__()

# 曲线特征提取
self.curve_encoder = nn.Sequential(
nn.Linear(200, 128), # 100 points * 2
nn.ReLU(inplace=True),
nn.Linear(128, 64),
nn.ReLU(inplace=True),
)

# 分类器
self.classifier = nn.Sequential(
nn.Linear(64 + 12, 32), # curve feat + keypoints
nn.ReLU(inplace=True),
nn.Linear(32, num_classes),
)

self.num_classes = num_classes

def forward(
self,
belt_curve: torch.Tensor,
keypoints: torch.Tensor
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
belt_curve: (B, 100, 2)
keypoints: (B, 6, 2)

Returns:
usage_type: (B,) 使用状态分类
confidence: (B,) 置信度
"""
# 提取曲线特征
curve_flat = belt_curve.view(belt_curve.shape[0], -1)
curve_feat = self.curve_encoder(curve_flat)

# 拼接关键点
keypoints_flat = keypoints.view(keypoints.shape[0], -1)
feat = torch.cat([curve_feat, keypoints_flat], dim=1)

# 分类
logits = self.classifier(feat)

usage_type = torch.argmax(logits, dim=1)
confidence = F.softmax(logits, dim=1).max(dim=1)[0]

return usage_type, confidence


# 测试代码
if __name__ == "__main__":
# 初始化模型
model = SeatbeltDetector({})
model.eval()

# 模拟输入
image = torch.randn(1, 3, 480, 640)

# 推理
with torch.no_grad():
result = model(image)

print(f"关键点: {result['keypoints'].shape}")
print(f"使用状态: {result['usage_type']}")
print(f"置信度: {result['confidence']}")

误用检测逻辑

分类标准

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
class SeatbeltMisuseDetector:
"""
安全带误用检测器

分类规则:
0. 正确佩戴
1. 未系安全带
2. 安全带在身后
3. 安全带在腋下
4. 安全带松弛
"""

MISUSE_NAMES = [
'正确佩戴',
'未系安全带',
'安全带在身后',
'安全带在腋下',
'安全带松弛'
]

def __init__(self, model_path: str):
self.model = SeatbeltDetector({})
self.model.load_state_dict(torch.load(model_path))
self.model.eval()

# 检测阈值
self.confidence_threshold = 0.7

def detect(self, image: np.ndarray) -> dict:
"""
检测安全带使用状态

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

Returns:
result: {
'misuse_type': 误用类型,
'is_correct': 是否正确佩戴,
'confidence': 置信度,
'keypoints': 关键点坐标
}
"""
# 预处理
x = self._preprocess(image)

# 推理
with torch.no_grad():
result = self.model(x)

usage_type = result['usage_type'][0].item()
confidence = result['confidence'][0].item()

return {
'misuse_type': self.MISUSE_NAMES[usage_type],
'is_correct': usage_type == 0,
'confidence': confidence,
'keypoints': result['keypoints'][0].cpu().numpy(),
'alert_level': self._get_alert_level(usage_type)
}

def _preprocess(self, image: np.ndarray) -> torch.Tensor:
"""预处理"""
x = cv2.resize(image, (640, 480))
x = x.astype(np.float32) / 255.0
x = (x - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
x = torch.from_numpy(x).permute(2, 0, 1).unsqueeze(0)
return x

def _get_alert_level(self, usage_type: int) -> int:
"""获取警告等级"""
if usage_type == 0:
return 0 # 无警告
elif usage_type in [1, 2]: # 未系、身后
return 2 # 二级警告
else:
return 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
class IRAdaptation(nn.Module):
"""
红外图像适配模块

解决:
1. 无颜色信息
2. 对比度低
3. 噪声较大
"""

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

# 对比度增强
self.contrast_enhance = nn.Sequential(
nn.Conv2d(1, 32, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(32, 1, 3, 1, 1),
)

# RGB生成(伪彩色)
self.ir_to_rgb = nn.Sequential(
nn.Conv2d(1, 16, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(16, 32, 3, 1, 1),
nn.ReLU(inplace=True),
nn.Conv2d(32, 3, 3, 1, 1),
)

def forward(self, ir_image: torch.Tensor) -> torch.Tensor:
"""
Args:
ir_image: (B, 1, H, W) 红外图像

Returns:
rgb_image: (B, 3, H, W) 伪RGB图像
"""
# 对比度增强
enhanced = self.contrast_enhance(ir_image)
enhanced = ir_image + enhanced

# 生成RGB
rgb = self.ir_to_rgb(enhanced)

return rgb

鱼眼畸变校正

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
class FisheyeCorrection:
"""
鱼眼镜头畸变校正

DMS常用大FOV镜头,需要校正
"""

def __init__(self, calibration_file: str):
# 加载标定参数
calib = np.load(calibration_file)
self.K = calib['K'] # 内参矩阵
self.D = calib['D'] # 畸变系数
self.dim = calib['dim'] # 图像尺寸

def undistort(self, image: np.ndarray) -> np.ndarray:
"""
去畸变

Args:
image: (H, W, 3) 畸变图像

Returns:
undistorted: (H, W, 3) 校正后图像
"""
import cv2

# 鱼眼模型
map1, map2 = cv2.fisheye.initUndistortRectifyMap(
self.K, self.D, None, self.K, self.dim, cv2.CV_16SC2
)

# 重映射
undistorted = cv2.remap(
image, map1, map2,
interpolation=cv2.INTER_LINEAR,
borderMode=cv2.BORDER_CONSTANT
)

return undistorted

实验结果

DMS场景测试

场景 检测准确率 误报率 时延
白天正确佩戴 98.5% 0.8% 15ms
白天未系 99.2% 0.5% 12ms
夜间红外 96.8% 2.1% 18ms
遮挡(手/头发) 94.3% 3.2% 20ms
安全带在身后 97.1% 1.8% 16ms

OMS场景测试

位置 检测准确率 备注
前排驾驶员 98.5% DMS摄像头
前排乘客 97.2% OMS摄像头
后排左 95.6% 鱼眼镜头
后排右 95.8% 鱼眼镜头

IMS应用启示

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
class ENCAPSeatbeltMonitor:
"""
Euro NCAP 安全带监控模块
"""

def __init__(self):
self.detector = SeatbeltMisuseDetector('seatbelt_dms.pth')
self.ir_adapter = IRAdaptation()

# 历史记录(用于时序过滤)
self.history = []
self.history_length = 10

def monitor(self, frame: np.ndarray, is_ir: bool = False) -> dict:
"""
监控安全带状态

Args:
frame: 输入帧
is_ir: 是否红外图像

Returns:
alert: 警告信息
"""
# 红外适配
if is_ir:
frame = self.ir_adapter(frame)

# 检测
result = self.detector.detect(frame)

# 记录历史
self.history.append(result)
if len(self.history) > self.history_length:
self.history.pop(0)

# 时序过滤(避免瞬时误检)
if len(self.history) >= 5:
misuse_count = sum(1 for h in self.history[-5:] if not h['is_correct'])
if misuse_count >= 3: # 5帧中至少3帧异常
result['alert'] = True

return result

参考资料

  1. Hu et al., “Robust Seatbelt Detection and Usage Recognition for Driver Monitoring Systems”, AAAI 2022
  2. Euro NCAP Assessment Protocol 2026
  3. NADS-Net: Nimble Architecture for Driver and Seat Belt Detection

关键词: 安全带检测, 安全带误用, DMS, OMS, Euro NCAP 2026

发布时间: 2026-07-21

作者: OpenClaw AI Research


安全带误用检测:DMS/OMS鲁棒方案
https://dapalm.com/2026/07/21/2026-07-21-seatbelt-misuse-detection-dms/
作者
Mars
发布于
2026年7月21日
许可协议