ViT-DD:多任务Vision Transformer半监督驾驶员分心检测

论文来源: arXiv 2209.09178, IEEE IV 2024
作者: Yunsheng Ma, Ziran Wang
核心创新: 多任务学习(分心检测+情绪识别)+ 半监督学习 + Vision Transformer


研究背景

分心检测的挑战

挑战 描述 传统方案局限
数据稀缺 标注成本高 需要大量人工标注
类别不平衡 某些分心行为罕见 模型偏向多数类
跨域泛化 不同驾驶员/环境差异大 泛化能力弱
多任务需求 分心+情绪等关联任务 单任务学习信息浪费

核心思路

1
2
3
4
5
6
7
8
9
传统方法:
图像 → CNN → 分心分类

ViT-DD方法:
图像 → Vision Transformer → 多任务头
├─ 分心检测
└─ 情绪识别

+ 半监督学习:无情绪标签数据也能参与训练

模型架构

ViT-DD整体结构

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
┌─────────────────────────────────────────────────────────────────┐
│ ViT-DD 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 输入图像 (224×224×3) │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ Patch Embedding │ │
│ │ - 16×16 patches │ │
│ │ - Linear projection │ │
│ │ - Position embedding │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ Vision Transformer Encoder │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Transformer Block × 12 │ │ │
│ │ │ - Multi-Head Self-Attention │ │ │
│ │ │ - Layer Norm │ │ │
│ │ │ - MLP │ │ │
│ │ │ - Residual Connection │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ CLS Token │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────┬───────────────────┐ │
│ │ 分心检测头 │ 情绪识别头 │ │
│ │ (10 classes) │ (7 classes) │ │
│ │ - Linear │ - Linear │ │
│ │ - Softmax │ - Softmax │ │
│ └───────────────────┴───────────────────┘ │
│ ↓ ↓ │
│ 分心类别 情绪类别 │
│ │
└─────────────────────────────────────────────────────────────────┘

分心检测类别

类别编号 描述 Euro NCAP场景
C0 正常驾驶 -
C1 右手打电话 D-02
C2 右手发短信 D-03
C3 左手打电话 D-02
C4 左手发短信 D-03
C5 调整收音机 D-05
C6 喝水 D-04
C7 拿后座物品 D-07
C8 整理头发/化妆 D-04
C9 与乘客交谈 D-08

情绪识别类别

类别 描述
Angry 愤怒
Disgust 厌恶
Fear 恐惧
Happy 快乐
Sad 悲伤
Surprise 惊讶
Neutral 中性

核心代码实现

1. Vision Transformer 骨干网络

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
"""
Vision Transformer for Driver Distraction Detection
基于ViT的多任务分心检测网络
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, Tuple
import math


class PatchEmbedding(nn.Module):
"""
图像Patch嵌入

将图像分割为patches并线性投影
"""

def __init__(
self,
image_size: int = 224,
patch_size: int = 16,
in_channels: int = 3,
embed_dim: int = 768
):
super().__init__()

self.image_size = image_size
self.patch_size = patch_size
self.num_patches = (image_size // patch_size) ** 2

# 卷积实现patch投影
self.proj = nn.Conv2d(
in_channels, embed_dim,
kernel_size=patch_size,
stride=patch_size
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch, channels, height, width)

Returns:
embeddings: (batch, num_patches, embed_dim)
"""
# (B, C, H, W) -> (B, embed_dim, H/P, W/P)
x = self.proj(x)

# (B, embed_dim, num_patches**0.5, num_patches**0.5)
# -> (B, num_patches, embed_dim)
x = x.flatten(2).transpose(1, 2)

return x


class MultiHeadAttention(nn.Module):
"""
多头自注意力机制
"""

def __init__(
self,
embed_dim: int = 768,
num_heads: int = 12,
dropout: float = 0.0
):
super().__init__()

self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads

assert self.head_dim * num_heads == embed_dim

self.qkv = nn.Linear(embed_dim, embed_dim * 3)
self.proj = nn.Linear(embed_dim, embed_dim)
self.dropout = nn.Dropout(dropout)

self.scale = self.head_dim ** -0.5

def forward(
self,
x: torch.Tensor,
mask: Optional[torch.Tensor] = None
) -> torch.Tensor:
"""
Args:
x: (batch, seq_len, embed_dim)
mask: attention mask

Returns:
output: (batch, seq_len, embed_dim)
"""
B, N, C = x.shape

# QKV投影
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim)
qkv = qkv.permute(2, 0, 3, 1, 4) # (3, B, heads, N, head_dim)
q, k, v = qkv[0], qkv[1], qkv[2]

# 注意力计算
attn = (q @ k.transpose(-2, -1)) * self.scale

if mask is not None:
attn = attn.masked_fill(mask == 0, float('-inf'))

attn = attn.softmax(dim=-1)
attn = self.dropout(attn)

# 输出
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
x = self.proj(x)

return x


class TransformerBlock(nn.Module):
"""
Transformer编码器块
"""

def __init__(
self,
embed_dim: int = 768,
num_heads: int = 12,
mlp_ratio: float = 4.0,
dropout: float = 0.0
):
super().__init__()

# Layer Norm
self.norm1 = nn.LayerNorm(embed_dim)
self.norm2 = nn.LayerNorm(embed_dim)

# Self-Attention
self.attn = MultiHeadAttention(
embed_dim=embed_dim,
num_heads=num_heads,
dropout=dropout
)

# MLP
mlp_hidden_dim = int(embed_dim * mlp_ratio)
self.mlp = nn.Sequential(
nn.Linear(embed_dim, mlp_hidden_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(mlp_hidden_dim, embed_dim),
nn.Dropout(dropout)
)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch, seq_len, embed_dim)

Returns:
output: (batch, seq_len, embed_dim)
"""
# Self-Attention with residual
x = x + self.attn(self.norm1(x))

# MLP with residual
x = x + self.mlp(self.norm2(x))

return x


class VisionTransformer(nn.Module):
"""
Vision Transformer骨干网络
"""

def __init__(
self,
image_size: int = 224,
patch_size: int = 16,
in_channels: int = 3,
embed_dim: int = 768,
depth: int = 12,
num_heads: int = 12,
mlp_ratio: float = 4.0,
dropout: float = 0.1
):
super().__init__()

# Patch Embedding
self.patch_embed = PatchEmbedding(
image_size=image_size,
patch_size=patch_size,
in_channels=in_channels,
embed_dim=embed_dim
)

num_patches = self.patch_embed.num_patches

# CLS Token
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))

# Position Embedding
self.pos_embed = nn.Parameter(
torch.zeros(1, num_patches + 1, embed_dim)
)
self.pos_drop = nn.Dropout(dropout)

# Transformer Blocks
self.blocks = nn.ModuleList([
TransformerBlock(
embed_dim=embed_dim,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
dropout=dropout
)
for _ in range(depth)
])

# Final Layer Norm
self.norm = nn.LayerNorm(embed_dim)

# 初始化
nn.init.trunc_normal_(self.cls_token, std=0.02)
nn.init.trunc_normal_(self.pos_embed, std=0.02)

def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Args:
x: (batch, channels, height, width)

Returns:
features: (batch, embed_dim) CLS token特征
"""
B = x.size(0)

# Patch embedding
x = self.patch_embed(x)

# 添加CLS token
cls_tokens = self.cls_token.expand(B, -1, -1)
x = torch.cat([cls_tokens, x], dim=1)

# 添加位置编码
x = x + self.pos_embed
x = self.pos_drop(x)

# Transformer blocks
for block in self.blocks:
x = block(x)

# Layer norm
x = self.norm(x)

# 返回CLS token
return x[:, 0]

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
"""
多任务学习模块
分心检测 + 情绪识别联合学习
"""

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


class MultiTaskHead(nn.Module):
"""
多任务输出头

同时预测分心类别和情绪类别
"""

def __init__(
self,
embed_dim: int = 768,
num_distraction_classes: int = 10,
num_emotion_classes: int = 7,
hidden_dim: int = 256
):
super().__init__()

# 分心检测头
self.distraction_head = nn.Sequential(
nn.Linear(embed_dim, hidden_dim),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(hidden_dim, num_distraction_classes)
)

# 情绪识别头
self.emotion_head = nn.Sequential(
nn.Linear(embed_dim, hidden_dim),
nn.ReLU(inplace=True),
nn.Dropout(0.3),
nn.Linear(hidden_dim, num_emotion_classes)
)

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

Returns:
distraction_logits: (batch, num_distraction_classes)
emotion_logits: (batch, num_emotion_classes)
"""
distraction_logits = self.distraction_head(x)
emotion_logits = self.emotion_head(x)

return distraction_logits, emotion_logits


class ViTDD(nn.Module):
"""
ViT-DD: Vision Transformer for Driver Distraction Detection

多任务半监督分心检测模型
"""

def __init__(
self,
image_size: int = 224,
patch_size: int = 16,
embed_dim: int = 768,
depth: int = 12,
num_heads: int = 12,
num_distraction_classes: int = 10,
num_emotion_classes: int = 7
):
super().__init__()

# ViT骨干
self.backbone = VisionTransformer(
image_size=image_size,
patch_size=patch_size,
embed_dim=embed_dim,
depth=depth,
num_heads=num_heads
)

# 多任务头
self.heads = MultiTaskHead(
embed_dim=embed_dim,
num_distraction_classes=num_distraction_classes,
num_emotion_classes=num_emotion_classes
)

def forward(
self,
x: torch.Tensor
) -> Dict[str, torch.Tensor]:
"""
Args:
x: (batch, channels, height, width)

Returns:
output: dict with 'distraction' and 'emotion' logits
"""
# 提取特征
features = self.backbone(x)

# 多任务预测
distraction_logits, emotion_logits = self.heads(features)

return {
'distraction': distraction_logits,
'emotion': emotion_logits,
'features': features
}

def predict_distraction(self, x: torch.Tensor) -> torch.Tensor:
"""
预测分心类别

Args:
x: (batch, channels, height, width)

Returns:
predictions: (batch,) 类别索引
"""
self.eval()
with torch.no_grad():
output = self.forward(x)
predictions = output['distraction'].argmax(dim=1)
return predictions

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
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
"""
半监督学习框架
利用无情绪标签数据增强训练
"""

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


class SemiSupervisedLoss(nn.Module):
"""
半监督多任务损失

组合:
1. 监督损失(有标签数据)
2. 伪标签损失(无标签数据)
3. 一致性正则化
"""

def __init__(
self,
distraction_weight: float = 1.0,
emotion_weight: float = 0.5,
consistency_weight: float = 0.1,
pseudo_label_threshold: float = 0.9
):
super().__init__()

self.distraction_weight = distraction_weight
self.emotion_weight = emotion_weight
self.consistency_weight = consistency_weight
self.threshold = pseudo_label_threshold

self.ce_loss = nn.CrossEntropyLoss()

def forward(
self,
output: Dict[str, torch.Tensor],
distraction_labels: torch.Tensor,
emotion_labels: Optional[torch.Tensor] = None,
augmented_output: Optional[Dict[str, torch.Tensor]] = None
) -> Tuple[torch.Tensor, Dict[str, float]]:
"""
计算总损失

Args:
output: 模型输出
distraction_labels: 分心标签
emotion_labels: 情绪标签(可选)
augmented_output: 增强后的输出(用于一致性)

Returns:
total_loss: 总损失
loss_dict: 各项损失
"""
losses = {}

# 1. 分心检测损失(总是有标签)
distraction_loss = self.ce_loss(
output['distraction'],
distraction_labels
)
losses['distraction'] = distraction_loss.item()

# 2. 情绪识别损失(可选)
if emotion_labels is not None:
emotion_loss = self.ce_loss(
output['emotion'],
emotion_labels
)
losses['emotion'] = emotion_loss.item()
else:
# 伪标签损失
emotion_probs = F.softmax(output['emotion'], dim=1)
max_probs, pseudo_labels = emotion_probs.max(dim=1)

# 只使用高置信度预测
mask = max_probs > self.threshold

if mask.sum() > 0:
emotion_loss = self.ce_loss(
output['emotion'][mask],
pseudo_labels[mask]
) * 0.5 # 降低权重
losses['emotion_pseudo'] = emotion_loss.item()
else:
emotion_loss = 0.0

# 3. 一致性正则化
if augmented_output is not None:
consistency_loss = F.kl_div(
F.log_softmax(output['distraction'], dim=1),
F.softmax(augmented_output['distraction'], dim=1),
reduction='batchmean'
)
losses['consistency'] = consistency_loss.item()
else:
consistency_loss = 0.0

# 总损失
total_loss = (
self.distraction_weight * distraction_loss +
self.emotion_weight * (emotion_loss if isinstance(emotion_loss, torch.Tensor) else torch.tensor(0.0)) +
self.consistency_weight * (consistency_loss if isinstance(consistency_loss, torch.Tensor) else torch.tensor(0.0))
)

return total_loss, losses


class SemiSupervisedTrainer:
"""
半监督训练器
"""

def __init__(
self,
model: ViTDD,
learning_rate: float = 1e-4,
weight_decay: float = 0.05,
warmup_epochs: int = 5,
total_epochs: int = 100
):
self.model = model
self.criterion = SemiSupervisedLoss()

# 优化器
self.optimizer = torch.optim.AdamW(
model.parameters(),
lr=learning_rate,
weight_decay=weight_decay
)

# 学习率调度
self.scheduler = self._create_scheduler(
warmup_epochs, total_epochs, learning_rate
)

def _create_scheduler(
self,
warmup_epochs: int,
total_epochs: int,
base_lr: float
):
"""创建余弦退火学习率调度器"""
from torch.optim.lr_scheduler import LambdaLR

def lr_lambda(epoch):
if epoch < warmup_epochs:
return epoch / warmup_epochs
else:
progress = (epoch - warmup_epochs) / (total_epochs - warmup_epochs)
return 0.5 * (1 + np.cos(np.pi * progress))

return LambdaLR(self.optimizer, lr_lambda)

def train_step(
self,
images: torch.Tensor,
distraction_labels: torch.Tensor,
emotion_labels: Optional[torch.Tensor] = None,
augmented_images: Optional[torch.Tensor] = None
) -> Dict[str, float]:
"""
单步训练

Args:
images: 输入图像
distraction_labels: 分心标签
emotion_labels: 情绪标签(可选)
augmented_images: 增强图像(用于一致性)

Returns:
losses: 损失字典
"""
self.model.train()

# 前向传播
output = self.model(images)

# 增强图像前向传播
augmented_output = None
if augmented_images is not None:
with torch.no_grad():
augmented_output = self.model(augmented_images)

# 计算损失
total_loss, loss_dict = self.criterion(
output,
distraction_labels,
emotion_labels,
augmented_output
)

# 反向传播
self.optimizer.zero_grad()
total_loss.backward()
self.optimizer.step()

return loss_dict

def validate(
self,
dataloader: torch.utils.data.DataLoader,
device: str = 'cuda'
) -> Dict[str, float]:
"""
验证模型

Args:
dataloader: 验证数据加载器
device: 设备

Returns:
metrics: 验证指标
"""
self.model.eval()

correct_distraction = 0
correct_emotion = 0
total = 0

with torch.no_grad():
for batch in dataloader:
images = batch['image'].to(device)
distraction_labels = batch['distraction'].to(device)

output = self.model(images)

pred_distraction = output['distraction'].argmax(dim=1)
correct_distraction += (pred_distraction == distraction_labels).sum().item()

if 'emotion' in batch:
emotion_labels = batch['emotion'].to(device)
pred_emotion = output['emotion'].argmax(dim=1)
correct_emotion += (pred_emotion == emotion_labels).sum().item()

total += images.size(0)

return {
'distraction_accuracy': correct_distraction / total,
'emotion_accuracy': correct_emotion / total if correct_emotion > 0 else 0.0
}


# 测试代码
if __name__ == "__main__":
# 创建模型
model = ViTDD(
image_size=224,
patch_size=16,
embed_dim=768,
depth=12,
num_heads=12,
num_distraction_classes=10,
num_emotion_classes=7
)

# 模拟数据
batch_size = 4
images = torch.randn(batch_size, 3, 224, 224)

# 前向传播
output = model(images)

print("=== ViT-DD 测试 ===")
print(f"输入形状: {images.shape}")
print(f"分心输出形状: {output['distraction'].shape}")
print(f"情绪输出形状: {output['emotion'].shape}")
print(f"特征形状: {output['features'].shape}")
print(f"总参数量: {sum(p.numel() for p in model.parameters()):,}")

# 半监督训练测试
trainer = SemiSupervisedTrainer(model)

distraction_labels = torch.randint(0, 10, (batch_size,))
emotion_labels = torch.randint(0, 7, (batch_size,))

losses = trainer.train_step(images, distraction_labels, emotion_labels)
print(f"\n训练损失: {losses}")

实验结果

数据集

数据集 样本数 类别数 描述
SFDDD 22,424 10 State Farm分心检测数据集
AUCDD 12,580 10 AUC分心检测数据集

性能对比

方法 SFDDD准确率 AUCDD准确率 参数量
ResNet-50 92.1% 89.3% 25.6M
EfficientNet-B4 93.5% 90.8% 19.0M
ViT-Base 94.2% 91.5% 86.6M
ViT-DD 95.8% 92.4% 86.7M

半监督学习效果

设置 有情绪标签 无情绪标签 准确率
监督学习 100% 0% 94.2%
半监督 50% 50% 95.1%
半监督 30% 70% 95.8%

IMS 开发启示

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
# IMS多任务模型设计
class IMSMultiTaskModel(nn.Module):
"""
IMS多任务模型

同时处理:
- 疲劳检测
- 分心检测
- 情绪识别
- 视线估计
"""

def __init__(self):
self.backbone = VisionTransformer()

# 多任务头
self.heads = nn.ModuleDict({
'fatigue': nn.Linear(768, 3), # 清醒/轻度/重度
'distraction': nn.Linear(768, 10),
'emotion': nn.Linear(768, 7),
'gaze': nn.Linear(768, 2) # pitch, yaw
})

def forward(self, x):
features = self.backbone(x)
return {name: head(features) for name, head in self.heads.items()}

2. Euro NCAP场景映射

Euro NCAP场景 ViT-DD类别 检测方法
D-02 手机通话 C1, C3 手持手机检测
D-03 手机打字 C2, C4 低头看手机
D-04 饮食 C6, C8 手部动作
D-05 调整设备 C5 伸手操作
D-07 搜索物品 C7 回头/弯腰
D-08 乘客交互 C9 回头交流

3. 部署优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 模型量化部署
def quantize_vit_dd(model):
"""动态量化ViT-DD模型"""
quantized_model = torch.quantization.quantize_dynamic(
model,
{nn.Linear},
dtype=torch.qint8
)
return quantized_model

# 模型大小对比
# 原始: 330MB (FP32)
# 量化后: 85MB (INT8)
# 推理速度: 2-3x 加速

总结

维度 内容
核心创新 多任务学习 + 半监督学习
性能提升 SFDDD +6.5%, AUCDD +0.9%
关键技术 Vision Transformer + 伪标签
IMS应用 多任务检测、跨域泛化
部署优化 量化后85MB,实时推理

发布时间: 2026-04-22
标签: #Vision Transformer #分心检测 #多任务学习 #半监督 #IMS


ViT-DD:多任务Vision Transformer半监督驾驶员分心检测
https://dapalm.com/2026/04/22/2026-04-22-vit-dd-semi-supervised-distraction/
作者
Mars
发布于
2026年4月22日
许可协议