安全带错误佩戴检测:Euro NCAP 2026新要求与视觉方案

Euro NCAP状态: 2026新增要求,检测belt misuse
检测难点: 肩带位置错误、腰带位置错误、缠绕
技术路线: 关键点检测 + 规则判断 + 深度学习分类


Euro NCAP Belt Misuse要求

错误佩戴类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────────┐
│ 安全带错误佩戴类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
Type 1: 肩带位置错误 │
│ ├─ 肩带位于手臂下方 │
│ ├─ 肩带位于背部 │
│ └─ 肩带未穿过肩膀 │
│ │
Type 2: 腰带位置错误 │
│ ├─ 腰带位于腹部(应位于髋骨) │
│ └─ 腰带过松 │
│ │
Type 3: 缠绕/扭曲 │
│ ├─ 安全带扭曲 │
│ ├─ 安全带缠绕身体部位 │
│ └─ 多人共用一条安全带 │
│ │
Type 4: 未系安全带 │
│ └─ 完全未佩戴 │
│ │
└─────────────────────────────────────────────────────────────────┘

检测场景要求

场景 描述 检测时限 警告等级
BM-01 肩带在手臂下 ≤3s 一级警告
BM-02 肩带在背后 ≤3s 一级警告
BM-03 腰带在腹部 ≤5s 二级警告
BM-04 安全带扭曲 ≤5s 二级警告
BM-05 未系安全带 ≤3s 一级警告

检测方法对比

方法 准确率 实时性 适用场景
关键点检测 92% 30fps 标准坐姿
语义分割 88% 25fps 遮挡场景
分类网络 95% 45fps 简单场景
多任务融合 96% 20fps 复杂场景

核心代码实现

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
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
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
"""
安全带关键点检测模型
检测安全带的几何位置判断是否正确佩戴
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Dict, List, Tuple, Optional
import numpy as np


class BeltKeypointDetector(nn.Module):
"""
安全带关键点检测器

检测关键点:
- 肩带起点(D环位置)
- 肩带肩部位置
- 肩带胸口位置
- 腰带左侧位置
- 腰带右侧位置
- 锁扣位置
"""

NUM_KEYPOINTS = 6

KEYPOINT_NAMES = [
'shoulder_anchor', # 肩带锚点(D环)
'shoulder_point', # 肩带穿过肩膀位置
'chest_point', # 肩带胸口位置
'lap_left', # 腰带左侧
'lap_right', # 腰带右侧
'buckle' # 锁扣
]

def __init__(
self,
backbone: str = 'resnet18',
pretrained: bool = True
):
super().__init__()

# 骨干网络
if backbone == 'resnet18':
from torchvision.models import resnet18
self.backbone = resnet18(pretrained=pretrained)
self.backbone.fc = nn.Identity()
feature_dim = 512
elif backbone == 'mobilenetv3':
from torchvision.models import mobilenet_v3_small
self.backbone = mobilenet_v3_small(pretrained=pretrained)
self.backbone.classifier = nn.Identity()
feature_dim = 576
else:
raise ValueError(f"Unknown backbone: {backbone}")

# 关键点回归头
self.keypoint_head = nn.Sequential(
nn.Linear(feature_dim, 256),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(256, self.NUM_KEYPOINTS * 2) # (x, y) per keypoint
)

# 可见性预测头
self.visibility_head = nn.Sequential(
nn.Linear(feature_dim, 128),
nn.ReLU(inplace=True),
nn.Linear(128, self.NUM_KEYPOINTS),
nn.Sigmoid()
)

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

Returns:
keypoints: (batch, num_keypoints, 2) 归一化坐标
visibility: (batch, num_keypoints) 可见性概率
"""
# 骨干特征
features = self.backbone(x)

# 关键点回归
keypoints = self.keypoint_head(features)
keypoints = keypoints.view(-1, self.NUM_KEYPOINTS, 2)
keypoints = torch.sigmoid(keypoints) # 归一化到[0, 1]

# 可见性
visibility = self.visibility_head(features)

return keypoints, visibility

def get_belt_geometry(
self,
keypoints: torch.Tensor,
visibility: torch.Tensor,
threshold: float = 0.5
) -> Dict:
"""
分析安全带几何关系

Args:
keypoints: (batch, 6, 2)
visibility: (batch, 6)
threshold: 可见性阈值

Returns:
geometry: 几何分析结果
"""
batch_size = keypoints.shape[0]
results = []

for b in range(batch_size):
kpts = keypoints[b].cpu().numpy()
vis = visibility[b].cpu().numpy()

# 只使用可见的关键点
valid_mask = vis > threshold

result = {
'keypoints': kpts,
'visibility': vis,
'valid_mask': valid_mask
}

# 计算几何特征
if valid_mask[0] and valid_mask[1] and valid_mask[2]:
# 肩带角度
shoulder_vec = kpts[1] - kpts[0]
chest_vec = kpts[2] - kpts[1]
angle = np.arctan2(shoulder_vec[1], shoulder_vec[0]) - \
np.arctan2(chest_vec[1], chest_vec[0])
result['shoulder_angle'] = np.degrees(angle)

if valid_mask[3] and valid_mask[4]:
# 腰带水平度
lap_vec = kpts[4] - kpts[3]
result['lap_angle'] = np.degrees(np.arctan2(lap_vec[1], lap_vec[0]))

results.append(result)

return results


class BeltMisuseClassifier(nn.Module):
"""
安全带错误佩戴分类器

分类类型:
0: 正确佩戴
1: 肩带位置错误
2: 腰带位置错误
3: 安全带扭曲
4: 未系安全带
"""

NUM_CLASSES = 5

CLASS_NAMES = [
'correct',
'shoulder_misuse',
'lap_misuse',
'twisted',
'not_worn'
]

def __init__(
self,
backbone: str = 'efficientnet_b0',
pretrained: bool = True
):
super().__init__()

# 骨干网络
from torchvision.models import efficientnet_b0
self.backbone = efficientnet_b0(pretrained=pretrained)

# 修改分类头
in_features = self.backbone.classifier[1].in_features
self.backbone.classifier[1] = nn.Linear(in_features, self.NUM_CLASSES)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch, 3, H, W)

Returns:
logits: (batch, num_classes)
"""
return self.backbone(x)

def predict(self, x: torch.Tensor) -> Tuple[int, float]:
"""预测类别"""
self.eval()
with torch.no_grad():
logits = self.forward(x)
probs = F.softmax(logits, dim=1)
pred_class = torch.argmax(probs, dim=1)
confidence = probs[0, pred_class]

return pred_class.item(), confidence.item()


class BeltMisuseDetector:
"""
安全带错误佩戴综合检测器

结合关键点检测和分类判断
"""

def __init__(
self,
keypoint_model_path: str,
classifier_model_path: str,
device: str = 'cpu'
):
"""
Args:
keypoint_model_path: 关键点模型路径
classifier_model_path: 分类模型路径
device: 设备
"""
self.device = device

# 加载关键点模型
self.keypoint_model = BeltKeypointDetector()
self.keypoint_model.load_state_dict(
torch.load(keypoint_model_path, map_location=device)
)
self.keypoint_model.to(device)
self.keypoint_model.eval()

# 加载分类模型
self.classifier = BeltMisuseClassifier()
self.classifier.load_state_dict(
torch.load(classifier_model_path, map_location=device)
)
self.classifier.to(device)
self.classifier.eval()

# 输入尺寸
self.input_size = (224, 224)

def preprocess(
self,
image: np.ndarray
) -> torch.Tensor:
"""预处理图像"""
import cv2

# 缩放
image = cv2.resize(image, self.input_size)

# BGR -> RGB
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 归一化
image = image.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
image = (image - mean) / std

# HWC -> CHW
image = image.transpose(2, 0, 1)

# 转tensor
tensor = torch.from_numpy(image).unsqueeze(0).float()

return tensor.to(self.device)

def detect(
self,
image: np.ndarray
) -> Dict:
"""
检测安全带佩戴状态

Args:
image: 输入图像

Returns:
result: 检测结果
"""
# 预处理
tensor = self.preprocess(image)

# 关键点检测
with torch.no_grad():
keypoints, visibility = self.keypoint_model(tensor)

# 分类
with torch.no_grad():
class_logits = self.classifier(tensor)
class_probs = F.softmax(class_logits, dim=1)
pred_class = torch.argmax(class_probs, dim=1).item()
confidence = class_probs[0, pred_class].item()

# 几何分析
geometry = self.keypoint_model.get_belt_geometry(keypoints, visibility)

# 规则判断(补充分类器)
rule_result = self._rule_based_check(keypoints[0].cpu().numpy(),
visibility[0].cpu().numpy())

# 融合结果
final_result = {
'classification': {
'class_id': pred_class,
'class_name': BeltMisuseClassifier.CLASS_NAMES[pred_class],
'confidence': confidence
},
'keypoints': {
'points': keypoints[0].cpu().numpy(),
'visibility': visibility[0].cpu().numpy(),
'geometry': geometry[0]
},
'rule_check': rule_result,
'is_correct': pred_class == 0 and rule_result['is_valid']
}

return final_result

def _rule_based_check(
self,
keypoints: np.ndarray,
visibility: np.ndarray
) -> Dict:
"""
基于规则的检查

Args:
keypoints: (6, 2) 关键点坐标
visibility: (6,) 可见性

Returns:
result: 规则检查结果
"""
issues = []

# 检查肩带角度
if visibility[0] > 0.5 and visibility[1] > 0.5 and visibility[2] > 0.5:
shoulder_vec = keypoints[1] - keypoints[0]
chest_vec = keypoints[2] - keypoints[1]

# 肩带应该斜向下
if shoulder_vec[1] < 0: # y向下为正
issues.append('shoulder_belt_direction_wrong')

# 角度过大可能表示肩带位置错误
angle = np.abs(np.arctan2(shoulder_vec[1], shoulder_vec[0]))
if angle > np.radians(60): # 超过60度
issues.append('shoulder_angle_abnormal')

# 检查腰带位置
if visibility[3] > 0.5 and visibility[4] > 0.5:
lap_vec = keypoints[4] - keypoints[3]

# 腰带应该基本水平
lap_angle = np.abs(np.arctan2(lap_vec[1], lap_vec[0]))
if lap_angle > np.radians(30): # 超过30度
issues.append('lap_belt_not_horizontal')

# 检查关键点是否都在合理位置
# 肩带肩部点应该在图像上半部分
if visibility[1] > 0.5:
if keypoints[1, 1] > 0.5: # y > 0.5 表示在下半部分
issues.append('shoulder_point_too_low')

return {
'is_valid': len(issues) == 0,
'issues': issues
}


# 测试
if __name__ == "__main__":
# 创建模型
keypoint_model = BeltKeypointDetector(backbone='resnet18')
classifier = BeltMisuseClassifier()

# 模拟输入
x = torch.randn(2, 3, 224, 224)

# 关键点检测测试
keypoints, visibility = keypoint_model(x)

print("=== 关键点检测测试 ===")
print(f"输入形状: {x.shape}")
print(f"关键点形状: {keypoints.shape}")
print(f"可见性形状: {visibility.shape}")

# 分类测试
logits = classifier(x)

print("\n=== 分类测试 ===")
print(f"输出形状: {logits.shape}")

# 几何分析
geometry = keypoint_model.get_belt_geometry(keypoints, visibility)

print("\n=== 几何分析 ===")
for i, g in enumerate(geometry):
print(f"样本{i}: 有效关键点数={np.sum(g['valid_mask'])}")

# 参数统计
print(f"\n关键点模型参数: {sum(p.numel() for p in keypoint_model.parameters()):,}")
print(f"分类模型参数: {sum(p.numel() for p in classifier.parameters()):,}")

2. 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
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
"""
Euro NCAP Belt Misuse测试场景生成
用于验证检测算法
"""

import numpy as np
from typing import Dict, List
import json


class BeltMisuseScenarioGenerator:
"""
安全带错误佩戴测试场景生成器
"""

# Euro NCAP定义的场景
SCENARIOS = {
"BM-01": {
"name": "肩带在手臂下方",
"description": "肩带从手臂下方穿过而非肩膀上方",
"misuse_type": "shoulder_under_arm",
"severity": "high",
"detection_requirements": {
"time_limit_sec": 3,
"warning_level": "primary"
}
},
"BM-02": {
"name": "肩带在背后",
"description": "肩带被放置在背后",
"misuse_type": "shoulder_behind_back",
"severity": "high",
"detection_requirements": {
"time_limit_sec": 3,
"warning_level": "primary"
}
},
"BM-03": {
"name": "腰带位置过高",
"description": "腰带位于腹部而非髋骨",
"misuse_type": "lap_too_high",
"severity": "medium",
"detection_requirements": {
"time_limit_sec": 5,
"warning_level": "secondary"
}
},
"BM-04": {
"name": "安全带扭曲",
"description": "安全带在佩戴时发生扭曲",
"misuse_type": "twisted",
"severity": "medium",
"detection_requirements": {
"time_limit_sec": 5,
"warning_level": "secondary"
}
},
"BM-05": {
"name": "未系安全带",
"description": "完全未佩戴安全带",
"misuse_type": "not_worn",
"severity": "high",
"detection_requirements": {
"time_limit_sec": 3,
"warning_level": "primary"
}
},
"BM-06": {
"name": "安全带过松",
"description": "安全带佩戴过松",
"misuse_type": "too_loose",
"severity": "low",
"detection_requirements": {
"time_limit_sec": 5,
"warning_level": "secondary"
}
}
}

def generate_test_cases(
self,
output_path: str,
variations_per_scenario: int = 10
) -> Dict:
"""
生成测试用例

Args:
output_path: 输出路径
variations_per_scenario: 每个场景的变体数量

Returns:
stats: 生成统计
"""
test_cases = []

for scenario_id, scenario in self.SCENARIOS.items():
for i in range(variations_per_scenario):
# 生成变体
variation = self._create_variation(scenario, i)

test_case = {
"test_id": f"{scenario_id}_{i:03d}",
"scenario_id": scenario_id,
"name": scenario["name"],
"misuse_type": scenario["misuse_type"],
"severity": scenario["severity"],
"variation": variation,
"expected_result": {
"detection_time_sec": scenario["detection_requirements"]["time_limit_sec"],
"warning_level": scenario["detection_requirements"]["warning_level"]
},
"test_conditions": {
"lighting": self._random_lighting(),
"clothing": self._random_clothing(),
"body_type": self._random_body_type(),
"seat_position": self._random_seat_position()
}
}
test_cases.append(test_case)

# 保存
output_file = f"{output_path}/belt_misuse_test_cases.json"
with open(output_file, 'w') as f:
json.dump(test_cases, f, indent=2)

return {
"total_cases": len(test_cases),
"scenarios": len(self.SCENARIOS),
"output_file": output_file
}

def _create_variation(
self,
scenario: Dict,
variation_id: int
) -> Dict:
"""创建场景变体"""
# 根据错误类型生成不同的关键点变体
misuse_type = scenario["misuse_type"]

# 标准关键点位置
base_keypoints = {
'shoulder_anchor': [0.7, 0.2],
'shoulder_point': [0.6, 0.3],
'chest_point': [0.5, 0.5],
'lap_left': [0.3, 0.7],
'lap_right': [0.7, 0.7],
'buckle': [0.5, 0.8]
}

if misuse_type == "shoulder_under_arm":
# 肩带在手臂下:肩部关键点位置偏低
base_keypoints['shoulder_point'] = [0.6, 0.5]

elif misuse_type == "shoulder_behind_back":
# 肩带在背后:肩部关键点不可见
base_keypoints['shoulder_point'] = [0.9, 0.1] # 超出画面

elif misuse_type == "lap_too_high":
# 腰带过高:腰带关键点位置偏高
base_keypoints['lap_left'] = [0.3, 0.5]
base_keypoints['lap_right'] = [0.7, 0.5]

elif misuse_type == "twisted":
# 扭曲:关键点位置交错
base_keypoints['chest_point'] = [0.4, 0.55] # 轻微偏移

elif misuse_type == "not_worn":
# 未系:所有关键点不可见
for k in base_keypoints:
base_keypoints[k] = [0.0, 0.0] # 不可见

return {
"keypoints": base_keypoints,
"variation_id": variation_id
}

def _random_lighting(self) -> str:
"""随机光照条件"""
return np.random.choice([
"daylight_bright",
"daylight_overcast",
"tunnel",
"night_interior_light",
"night_no_light"
])

def _random_clothing(self) -> str:
"""随机着装"""
return np.random.choice([
"tshirt",
"jacket",
"coat",
"sleeveless",
"dark_clothing",
"light_clothing"
])

def _random_body_type(self) -> str:
"""随机体型"""
return np.random.choice([
"slim",
"average",
"heavy",
"tall",
"short"
])

def _random_seat_position(self) -> str:
"""随机座椅位置"""
return np.random.choice([
"forward",
"middle",
"backward",
"reclined"
])


# 测试
if __name__ == "__main__":
generator = BeltMisuseScenarioGenerator()

stats = generator.generate_test_cases("./output", variations_per_scenario=5)

print("=== Belt Misuse测试用例生成 ===")
print(f"总用例数: {stats['total_cases']}")
print(f"场景类型: {stats['scenarios']}")
print(f"输出文件: {stats['output_file']}")

性能指标

检测准确率要求

错误类型 召回率要求 精度要求 F1-score
未系安全带 ≥98% ≥95% ≥96%
肩带位置错误 ≥95% ≥90% ≥92%
腰带位置错误 ≥90% ≥85% ≥87%
安全带扭曲 ≥85% ≥80% ≥82%

实时性要求

平台 推理延迟 帧率
QCS8255 ≤30ms ≥30fps
Snapdragon 8 Gen 2 ≤20ms ≥45fps
Jetson Orin ≤15ms ≥60fps

IMS开发建议

部署优先级

优先级 功能 说明
P0 未系安全带检测 基础功能,精度要求最高
P0 肩带位置检测 Euro NCAP强制要求
P1 腰带位置检测 2026新增要求
P2 安全带扭曲检测 可选增强功能

传感器配置

配置 摄像头 光源 适用场景
基础配置 RGB 2MP 自然光 白天
标准配置 RGB-IR 2MP 940nm IR 日夜
高端配置 RGB-IR + ToF 940nm IR 日夜+深度

总结

维度 内容
检测类型 肩带/腰带位置、扭曲、未系
技术路线 关键点+分类+规则融合
精度要求 召回率≥95%,精度≥90%
实时要求 ≤30ms延迟,≥30fps
Euro NCAP 2026新增,检测时限≤5s

发布时间: 2026-04-22
标签: #安全带检测 #BeltMisuse #EuroNCAP #IMS #计算机视觉


安全带错误佩戴检测:Euro NCAP 2026新要求与视觉方案
https://dapalm.com/2026/04/22/2026-04-22-seatbelt-misuse-detection/
作者
Mars
发布于
2026年4月22日
许可协议