对抗攻击

要求:

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
任务要求: 
完善 /home/work/adversarial_detection/main.py 文件中的 detect() 函数。

数据集介绍

特别说明:参赛选手不允许使用额外数据

本次靶场使用自定义数据集,该数据集共 300 张图片,图片格式为 bmp

数据集格式如下:

数据集总量为300张,数据集结构示例如下:

├── data

├── 000001.bmp
├── 000002.bmp
├──...
├── 000050.bmp
├──...






评价指标

在对数据对抗后,会输出一个 csv 文件,文件中记录了所有图像的文件名以及是否被对抗。未对抗标记为0,对抗图像标记为1。

在对抗检测后,也要输出一个 csv 文件,文件中记录了所有图像的文件名以及检测出的是否被对抗。检测结果中的未对抗图像标记为0,对抗图像标记为1。❗️❗️❗️ 该 csv 文件用于评分,非常重要。

csv文件中包括两列(没有表头),第一列为文件名,第二列为检测结果,内容示例如下:

001.jpg 0

002.jpg 1

会根据生成结果和检测结果,计算出选手的TP(True Positive)、TN(True Negative),并计算得到选手的最终准确率。

下载数据集,发现给的是源数据集…不告诉我扰动我怎么检测。

于是乎之际写个了FSGM脚本,加扰动,查看一下结果: image-20250825234029458

哦!发现个事啊兄弟们,这个玩意扰动越高,黑色素越少呢。有攻击方向就好办了,由于比赛联网,直接AI(话说Ai比赛用AI做,没毛病吧)。

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
def load_model(args):
"""
加载模型(参考方法),模型结构为 ResNet-50
:param args:
:return:
"""


def detect(args, model) -> list:
"""
换一种检测方法:结合模型鲁棒性检测 + 纯黑像素占比检测
返回格式:[['0.png', 0], ['1.png', 1], ...]
"""
# 设备
try:
device = next(model.parameters()).device
except StopIteration:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.eval()

# 预处理(ImageNet 标准)
def to_tensor_norm(pil_img: Image.Image) -> torch.Tensor:
t = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
return t(pil_img)

# 视角变换(用于鲁棒性一致性度量)
blur_transform = transforms.GaussianBlur(kernel_size=3, sigma=1.0)
slight_resize = transforms.Compose([
transforms.Resize(232),
transforms.CenterCrop(224),
])

# 阈值(可按验证集调优)
MIN_CONF_THRESHOLD = 0.50 # 基础预测最小置信度
DISAGREE_COUNT_THRESHOLD = 2 # 与基础预测不一致的视角数阈值
MEAN_CONF_DROP_THRESHOLD = 0.15 # 平均置信度下降阈值
BLACK_RATIO_THRESHOLD = 0.20 # 纯黑像素占比阈值(20%)

def is_image_file(name: str) -> bool:
return name.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".webp"))

def pil_open_rgb(path: str) -> Image.Image:
img = Image.open(path)
if img.mode != "RGB":
img = img.convert("RGB")
return img

def jpeg_compress(img: Image.Image, quality: int = 60) -> Image.Image:
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality)
buf.seek(0)
comp = Image.open(buf)
return comp.convert("RGB")

def compute_black_ratio(pil_img: Image.Image) -> float:
"""
计算原图(非缩放/标准化前)纯黑像素比例。
纯黑像素定义:RGB 三通道都为0。
"""
arr = np.asarray(pil_img)
# 形状 [H, W, 3]
if arr.ndim != 3 or arr.shape[2] != 3:
# 非预期情况,转为RGB后重试
arr = np.asarray(pil_img.convert("RGB"))
mask_black = (arr[:, :, 0] == 0) & (arr[:, :, 1] == 0) & (arr[:, :, 2] == 0)
ratio = float(mask_black.sum()) / float(mask_black.size)
return ratio

@torch.no_grad()
def predict_logits(t: torch.Tensor) -> torch.Tensor:
t = t.unsqueeze(0).to(device, dtype=torch.float32)
logits = model(t)
if isinstance(logits, (tuple, list)):
logits = logits[0]
return logits.squeeze(0)

@torch.no_grad()
def predict_probs(t: torch.Tensor):
logits = predict_logits(t)
probs = F.softmax(logits, dim=-1)
conf, pred = torch.max(probs, dim=-1)
return pred.item(), conf.item(), probs

def tensor_with_noise(base_tensor: torch.Tensor, sigma: float = 0.02) -> torch.Tensor:
torch.manual_seed(0) # 可重复
noise = torch.randn_like(base_tensor) * sigma
return torch.clamp(base_tensor + noise, min=-10.0, max=10.0)

results = []
dataset_path = os.path.abspath(args.dataset_path)
if not os.path.exists(dataset_path):
raise FileNotFoundError(f"数据集路径不存在: {dataset_path}")

# 收集文件
files = []
for root, _, filenames in os.walk(dataset_path):
for fn in filenames:
if is_image_file(fn):
files.append(os.path.join(root, fn))
files.sort()

logger.info(f"共发现 {len(files)} 个待检测文件")

processed = 0
for fp in files:
try:
pil_img = pil_open_rgb(fp)

# 1) 纯黑像素占比
black_ratio = compute_black_ratio(pil_img)

# 2) 模型鲁棒性一致性检测
base_tensor = to_tensor_norm(pil_img)
base_pred, base_conf, _ = predict_probs(base_tensor)

views = []
views.append(to_tensor_norm(blur_transform(pil_img))) # 模糊
views.append(to_tensor_norm(slight_resize(pil_img))) # 轻微缩放
views.append(to_tensor_norm(jpeg_compress(pil_img))) # JPEG压缩
views.append(tensor_with_noise(base_tensor, sigma=0.02))# 微噪声

disagree = 0
conf_drops = []
for vt in views:
v_pred, v_conf, _ = predict_probs(vt)
if v_pred != base_pred:
disagree += 1
conf_drops.append(max(0.0, base_conf - v_conf))

mean_conf_drop = float(np.mean(conf_drops)) if conf_drops else 0.0

# 子判据
is_adv_by_model = (disagree >= DISAGREE_COUNT_THRESHOLD) or \
((base_conf < MIN_CONF_THRESHOLD) and (mean_conf_drop >= MEAN_CONF_DROP_THRESHOLD))
is_adv_by_black = (black_ratio >= BLACK_RATIO_THRESHOLD)

# 融合策略:OR(任一子判据满足即判为对抗/异常)
# is_adv = 1 if (is_adv_by_model or is_adv_by_black) else 0
is_adv = 0 if is_adv_by_black else 1

results.append([os.path.basename(fp), is_adv])

processed += 1
if processed % 50 == 0 or processed == len(files):
logger.info(
f"进度: {processed}/{len(files)} | {os.path.basename(fp)} | "
f"black_ratio={black_ratio:.3f} | base_conf={base_conf:.3f} | "
f"disagree={disagree} | mean_drop={mean_conf_drop:.3f} | adv={is_adv}"
)

except Exception as e:
logger.error(f"处理文件失败: {fp} | 错误: {e}")
# 保守处理:标记为对抗
results.append([os.path.basename(fp), 1])

return results
1
2
3
```is_adv = 1 if (is_adv_by_model or is_adv_by_black) else 0``` -> 3分
```is_adv = 0 if (is_adv_by_model or is_adv_by_black) else 1``` -> 97分
```is_adv = 0 if is_adv_by_black else 1``` -> 100分

看来,那个破模型,真的很垃圾—

数据投毒

要求:

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
比赛任务

在本任务中,要求参赛者设计投毒检测算法,检测出所有图像中被投毒的图像。

任务要求:

请补全 /home/work/poison_detection/main.py 文件中 detect() 函数中缺失的部分。

数据集介绍

特别说明:参赛选手不允许使用额外数据

本次靶场使用自定义数据集,该数据集是 cifar-10 数据集的子集。

数据集格式如下:

数据集总量为200,并按标签分类,数据集结构示例如下:

├── data

│├── 0
│ ├── 000001.jpg
│ ├── 000002.jpg
│ ├──...
│ └── 000020.jpg
├── 1
│ ├── 000001.jpg
│ ├── 000002.jpg
│ ├──...
│ └── 000020.jpg
├──...
└── 9
├── 000001.jpg
├── 000002.jpg
├──...
└── 000020.jpg





评价指标

在对数据投毒后,会输出一个 csv 文件,文件中记录了所有图像的文件名以及是否被投毒。未投毒标记为0,投毒图像标记为1。

在投毒检测后,也要输出一个 csv 文件,文件中记录了所有图像的文件名以及检测出的是否被投毒。检测结果中的未投毒图像标记为0,投毒图像标记为1。❗️❗️❗️ 该 csv 文件用于评分,非常重要。

csv文件中包括两列(不含表头),第一列为文件名,第二列为检测结果,内容示例如下:

001.jpg 0

002.jpg 1

会根据投毒结果和检测结果,计算出选手的TP(True Positive)、TN(True Negative),并计算得到选手的最终准确率。

这个给了投毒之后的数据集:

image-20250825235007545

很明显的可以看到,加了个后门,有三条黑色横线。

print(im.mode)判断一下,无alpha通道。学姐通过比对,发现这个后门(黑线)相当于在对应区域叠了一层透明度约为 0.2 的黑色,效果等同于把那些区域的亮度乘以 0.8(变暗约 20%)/ 叠加 #00000033

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
def load_model(args):
"""
加载模型(参考方法),模型结构为 ResNet-18
:param args:
:return:
"""


def _first_conv_in_channels(model: nn.Module) -> int:
"""
尝试获取模型首个卷积层的输入通道数,失败则默认返回3
"""
try:
for m in model.modules():
if isinstance(m, nn.Conv2d):
return int(m.in_channels)
except Exception:
pass
return 3

def _ts_affine(pil_img: Image.Image, translate_px: int = 2) -> Image.Image:
"""
轻微平移变换
"""
w, h = pil_img.size
return pil_img.transform(
(w, h),
Image.AFFINE,
(1, 0, translate_px, 0, 1, translate_px),
resample=Image.BILINEAR
)


def _build_preprocess(in_ch: int, input_size: int = 224):
"""
构建基础预处理(resize + to tensor)
"""
tfs = []
# 模型若为单通道,先转成 L;否则统一转 RGB
if in_ch == 1:
tfs.append(transforms.Grayscale(num_output_channels=1))
else:
tfs.append(transforms.Lambda(lambda im: im.convert('RGB')))
tfs.extend([
transforms.Resize((input_size, input_size), interpolation=transforms.InterpolationMode.BILINEAR),
transforms.ToTensor(),
])
return transforms.Compose(tfs)


def _ensure_channels(t: torch.Tensor, in_ch: int) -> torch.Tensor:
"""
确保输入张量的通道数等于模型期望值。
- 如果模型要3通道而图像是1通道,则复制到3通道
- 如果模型要1通道而图像是3通道,则转灰度(取加权平均)
"""
if t.dim() == 3:
c = t.size(0)
if in_ch == 3 and c == 1:
t = t.repeat(3, 1, 1)
elif in_ch == 1 and c == 3:
r, g, b = t[0], t[1], t[2]
gray = 0.2989 * r + 0.5870 * g + 0.1140 * b
t = gray.unsqueeze(0)
return t


def _softmax_probs(model: nn.Module, x: torch.Tensor) -> torch.Tensor:
"""
前向计算并返回 softmax 概率(B,C)
"""
with torch.no_grad():
logits = model(x)
if logits.dim() > 2:
logits = logits.view(logits.size(0), -1)
return F.softmax(logits, dim=1)


def _js_divergence(p: torch.Tensor, q: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
"""
计算批量 Jensen-Shannon 散度(逐样本),返回形状 (B,)
p,q: (B,C) 且为概率分布
"""
m = 0.5 * (p + q)
kl_pm = torch.sum(p * (torch.log(p + eps) - torch.log(m + eps)), dim=1)
kl_qm = torch.sum(q * (torch.log(q + eps) - torch.log(m + eps)), dim=1)
js = 0.5 * (kl_pm + kl_qm)
return js


def _find_segments_from_mask(mask: np.ndarray, min_h: int = 2) -> List[Tuple[int, int, int]]:
"""
从布尔行掩码中找到连续的 True 段,返回列表:[ (start, end, length) ]
"""
if mask.size == 0:
return []
pad = np.r_[False, mask, False]
diff = np.diff(pad.astype(np.int8))
starts = np.where(diff == 1)[0]
ends = np.where(diff == -1)[0]
lengths = ends - starts
segs = []
for s, e, l in zip(starts, ends, lengths):
if l >= min_h:
segs.append((int(s), int(e), int(l))) # [s, e) 半开区间
return segs


def _three_black_stripes_features(pil_img: Image.Image) -> Tuple[float, int, float, float]:
"""
检测“三条横向均匀分布的黑色条纹”特征(图片无 alpha,叠加 #00000033 等效为乘以约 0.8 的亮度因子)
返回:
- rows_ratio: 条纹行数占比(总条纹行数 / 总行数)
- stripe_count: 选出的条纹段数量(0~3)
- avg_dark_ratio: 条纹行平均“变暗比” row_mean / baseline_mean,越低越可疑(理想约 0.8)
- uniformity: 三条条纹中心的间距均匀性 (0~1, 1 最均匀)
实现思路:
1) 转灰度,计算每行均值和标准差
2) 用行方向中值滤波估计“无条纹”的本地基线亮度 baseline
3) ratio = row_mean / baseline,ratio 低说明该行被整体变暗(贴黑条)
4) 同时约束行内标准差偏小,筛选为候选条纹行
5) 合并连续候选行为段,基于 ratio/std/厚度选择最多 3 段
6) 计算三段中心的间距均匀性
"""
g = pil_img.convert('L')
arr = np.asarray(g, dtype=np.float32)
H, W = arr.shape[:2]
if H == 0:
return 0.0, 0, 1.0, 0.0

row_mean = arr.mean(axis=1) # (H,)
row_std = arr.std(axis=1)

# 行向基线估计:优先用中值滤波,失败则用移动平均
try:
from scipy.ndimage import median_filter
k = max(7, int(H // 50) | 1) # 奇数窗口
baseline = median_filter(row_mean, size=k, mode='nearest')
except Exception:
k = max(7, int(H // 50))
if k % 2 == 0:
k += 1
pad = np.pad(row_mean, (k // 2, k // 2), mode='edge')
kernel = np.ones(k, dtype=np.float32) / float(k)
baseline = np.convolve(pad, kernel, mode='valid')

eps = 1e-6
ratio = row_mean / (baseline + eps) # 1.0 表示未变暗,~0.8 表示贴了 #00000033
# 候选条纹条件:整体变暗 且 行内像素较一致(小 std)
# 阈值取自适应:ratio <= 0.9(暗 10% 以上),且 std 小于全局行 std 的分位阈值
std_thr = float(np.percentile(row_std, 40))
candidate_mask = (ratio <= 0.9) & (row_std <= std_thr)

# 段厚度约束:期望条纹厚度占图高的 0.5%~8%
h_min = max(2, int(H * 0.005))
h_max = max(h_min + 1, int(H * 0.08))

segs = _find_segments_from_mask(candidate_mask, min_h=h_min)

# 为每段计算打分:越暗(1-ratio)、越均匀(低 std)、厚度落在区间内越好
seg_items = []
for s, e, l in segs:
seg_slice = slice(s, e)
mr = float(ratio[seg_slice].mean())
ms = float(row_std[seg_slice].mean())
# 归一化厚度适配(落在 [h_min,h_max] 得高分)
if l < h_min:
thick_fit = l / float(h_min)
elif l > h_max:
thick_fit = max(0.0, 1.0 - (l - h_max) / float(h_max))
else:
thick_fit = 1.0
# 标准差归一化(低 std 更接近均匀黑条)
ms_norm = 1.0 - (ms / (std_thr + 1e-6))
ms_norm = float(np.clip(ms_norm, 0.0, 1.0))
darkness = float(np.clip((1.0 - mr) / 0.3, 0.0, 1.0)) # mr≈0.8 -> darkness≈0.67
score = 0.55 * darkness + 0.25 * ms_norm + 0.20 * thick_fit
center = (s + e - 1) / 2.0
seg_items.append({
's': s, 'e': e, 'l': l, 'mr': mr, 'ms': ms,
'score': float(score), 'center': float(center)
})

# 选择最多 3 段,按分数从高到低,避免重叠
seg_items.sort(key=lambda d: d['score'], reverse=True)
selected = []
for it in seg_items:
if len(selected) >= 3:
break
overlap = False
for jt in selected:
if not (it['e'] <= jt['s'] or it['s'] >= jt['e']):
overlap = True
break
if not overlap:
selected.append(it)

selected.sort(key=lambda d: d['center'])
stripe_count = len(selected)
if stripe_count == 0:
return 0.0, 0, 1.0, 0.0

rows_total = sum([d['l'] for d in selected])
rows_ratio = rows_total / float(H)

avg_dark_ratio = float(np.mean([d['mr'] for d in selected])) # 越低越暗(理想≈0.8)

# 均匀性:三条条纹中心之间的间距接近
centers = [d['center'] / float(H) for d in selected] # 归一化到 [0,1]
if len(centers) >= 2:
gaps = np.diff(np.asarray(centers))
if len(gaps) == 1:
uniformity = 1.0 # 两条时无法评估,视为较均匀
else:
g1, g2 = float(gaps[0]), float(gaps[1])
uniformity = max(0.0, 1.0 - abs(g1 - g2) / (max(g1, g2, 1e-6)))
else:
uniformity = 0.0

return float(rows_ratio), int(stripe_count), float(avg_dark_ratio), float(uniformity)


def detect(args, model) -> list:
"""
检测数据集中被投毒的文件,并返回检测结果
:param args: 输入参数
:param model: 预训练模型
:return: 检测结果列表,列表的每个元素是一个子列表,子列表包含 2 列,
第一列为文件名,第二列为检测结果(0 或者 1,0-干净数据,1-投毒数据)
示例 [['0.png', 0], ['1.png', 1],...]
"""
results = []

try:
data_dir = os.path.abspath(args.poisoned_data_path)
if not os.path.isdir(data_dir):
raise FileNotFoundError(f"数据集目录不存在: {data_dir}")

# 递归收集图像文件(使用相对路径,最终写 CSV 时输出 basename 以匹配评分格式)
exts = {'.bmp', '.png', '.jpg', '.jpeg'}
files = []
for root, _, filenames in os.walk(data_dir):
for fn in filenames:
if os.path.splitext(fn)[1].lower() in exts:
rel = os.path.relpath(os.path.join(root, fn), data_dir)
rel = rel.replace(os.sep, '/')
files.append(rel)
files = sorted(files)

if len(files) == 0:
logger.warning(f"目录 {data_dir} 未找到图像文件(支持扩展名: {exts}),返回空结果。")
return []

model.eval()
device = _get_device(model)
in_ch = _first_conv_in_channels(model)

# 输入尺寸:优先使用模型定义的属性,否则默认 224
input_size = getattr(model, 'input_size', 224)
if not isinstance(input_size, int):
try:
input_size = int(input_size[0])
except Exception:
input_size = 224

preprocess = _build_preprocess(in_ch, input_size)

# 轻微增广,检测预测稳定性
aug_fns = [
lambda im: im,
lambda im: im.filter(ImageFilter.GaussianBlur(radius=0.8)),
lambda im: _jpeg_compress(im, quality=70),
lambda im: _ts_affine(im, translate_px=2),
]

# 特征收集:稳定性分数、梯度分数、条纹分数(考虑 #00000033 的乘性变暗)
feature_list: List[List[float]] = []
name_list: List[str] = []
strong_flags: List[bool] = []

logger.info(f"开始检测,共 {len(files)} 张图片")
for fname in tqdm(files, desc="Detecting", ncols=80):
fpath = os.path.join(data_dir, fname)
try:
im = Image.open(fpath).convert('RGB')
except Exception:
logger.warning(f"无法读取图像文件: {fpath},跳过。")
continue

# 1) 模型稳定性
x0 = preprocess(im)
x0 = _ensure_channels(x0, in_ch)
x0 = x0.unsqueeze(0).to(device)

p0 = _softmax_probs(model, x0).squeeze(0)
c0 = int(torch.argmax(p0).item())
p0c = float(p0[c0].item())

js_scores, drops = [], []
for afn in aug_fns[1:]:
xi = preprocess(afn(im))
xi = _ensure_channels(xi, in_ch)
xi = xi.unsqueeze(0).to(device)
pi = _softmax_probs(model, xi).squeeze(0)

js = float(_js_divergence(p0.unsqueeze(0), pi.unsqueeze(0)).item())
js_scores.append(js)

pic = float(pi[c0].item())
drops.append(max(0.0, p0c - pic))

js_mean = float(np.mean(js_scores)) if js_scores else 0.0
drop_mean = float(np.mean(drops)) if drops else 0.0
stability_score = js_mean + 0.5 * drop_mean

# 2) 输入梯度敏感度
x0_req = x0.detach().clone().requires_grad_(True)
logits = model(x0_req)
if logits.dim() > 2:
logits = logits.view(logits.size(0), -1)
loss = -F.log_softmax(logits, dim=1)[0, c0]
model.zero_grad(set_to_none=True)
loss.backward()
grad = x0_req.grad.detach()
grad_norm = float(torch.norm(grad.view(grad.size(0), -1), p=2, dim=1).item())
grad_score = float(np.log1p(grad_norm))

# 3) 三条横向黑条检测(无 alpha: 亮度约乘 0.8)
rows_ratio, stripe_cnt, avg_dark_ratio, uniformity = _three_black_stripes_features(im)
# 将条纹特征融合为分数:越多条纹、越暗(低 avg_dark_ratio)、越均匀,分数越高
# 期望 avg_dark_ratio≈0.8,这里将 (0.9 - x) 归一化处理
darkness_term = float(np.clip((0.9 - avg_dark_ratio) / 0.2, 0.0, 1.0))
stripe_score = 0.5 * darkness_term + 0.3 * uniformity + 0.2 * float(np.clip(rows_ratio / 0.08, 0.0, 1.0))
# 强规则:三条且较均匀且明显变暗
strong_stripe = (stripe_cnt >= 3 and uniformity >= 0.6 and avg_dark_ratio <= 0.88 and 0.01 <= rows_ratio <= 0.15)

feature_list.append([stability_score, grad_score, stripe_score])
name_list.append(fname)
strong_flags.append(bool(strong_stripe))

if len(feature_list) == 0:
logger.warning("未提取到任何样本特征,返回空结果。")
return []

X = np.array(feature_list, dtype=np.float32) # (N,3)

# 无监督 2 簇聚类:高均值簇视为可疑(1)
try:
gmm = GaussianMixture(n_components=2, covariance_type='full', random_state=0)
gmm.fit(X)
labels = gmm.predict(X)
means = gmm.means_.mean(axis=1)
suspicious_cluster = int(np.argmax(means))

for i, fname in enumerate(name_list):
pred = 1 if labels[i] == suspicious_cluster else 0
if pred == 0 and strong_flags[i]:
pred = 1
results.append([path.basename(fname), int(pred)])

except Exception as e:
logger.warning(f"GMM 聚类失败,使用阈值回退方案。错误: {repr(e)}")
S = X[:, 0] + X[:, 1] + 1.5 * X[:, 2]
mu, sigma = float(S.mean()), float(S.std() + 1e-6)
z = (S - mu) / sigma
thr = 0.0
for i, fname in enumerate(name_list):
pred = 1 if (z[i] > thr or strong_flags[i]) else 0
results.append([path.basename(fname), int(pred)])

logger.info("检测完成。")
return results

except Exception:
logger.error("检测流程发生异常:\n" + traceback.format_exc())
raise

还有一个,丢弃了题目中给的大模型,仅通过黑条判断:

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
def _three_black_stripes_features(pil_img: Image.Image) -> Tuple[float, int, float, float]:
"""
检测“三条横向均匀分布的黑色条纹”特征(图片无 alpha,#00000033 近似为亮度乘 ~0.8)
返回:
- rows_ratio: 条纹行数占比(总条纹行数 / 总行数)
- stripe_count: 选出的条纹段数量(0~3)
- avg_dark_ratio: 条纹行平均“变暗比” row_mean / baseline_mean,越低越可疑(理想约 0.8)
- uniformity: 三条条纹中心的间距均匀性 (0~1, 1 最均匀)
"""
g = pil_img.convert('L')
arr = np.asarray(g, dtype=np.float32)
H = arr.shape[0]
if H == 0:
return 0.0, 0, 1.0, 0.0

row_mean = arr.mean(axis=1) # (H,)
row_std = arr.std(axis=1)

# 行向基线估计:优先用中值滤波,失败则用移动平均
try:
from scipy.ndimage import median_filter
k = max(7, int(H // 50) | 1) # 奇数窗口
baseline = median_filter(row_mean, size=k, mode='nearest')
except Exception:
k = max(7, int(H // 50))
if k % 2 == 0:
k += 1
pad = np.pad(row_mean, (k // 2, k // 2), mode='edge')
kernel = np.ones(k, dtype=np.float32) / float(k)
baseline = np.convolve(pad, kernel, mode='valid')

eps = 1e-6
ratio = row_mean / (baseline + eps) # 1.0 表示未变暗,~0.8 表示贴了 #00000033

# 候选条纹条件:整体变暗 且 行内像素较一致(小 std)
std_thr = float(np.percentile(row_std, 40))
candidate_mask = (ratio <= 0.9) & (row_std <= std_thr)

# 段厚度约束:期望条纹厚度占图高的 0.5%~8%
h_min = max(2, int(H * 0.005))
h_max = max(h_min + 1, int(H * 0.08))

segs = _find_segments_from_mask(candidate_mask, min_h=h_min)

# 为每段计算打分:越暗(1-ratio)、越均匀(低 std)、厚度落在区间内越好
seg_items = []
for s, e, l in segs:
seg_slice = slice(s, e)
mr = float(ratio[seg_slice].mean()) # 变暗比,越小越暗
ms = float(row_std[seg_slice].mean()) # 行内均匀性,越小越均匀
# 厚度适配
if l < h_min:
thick_fit = l / float(h_min)
elif l > h_max:
thick_fit = max(0.0, 1.0 - (l - h_max) / float(h_max))
else:
thick_fit = 1.0
ms_norm = 1.0 - (ms / (std_thr + 1e-6))
ms_norm = float(np.clip(ms_norm, 0.0, 1.0))
darkness = float(np.clip((1.0 - mr) / 0.3, 0.0, 1.0)) # mr≈0.8 -> darkness≈0.67
score = 0.55 * darkness + 0.25 * ms_norm + 0.20 * thick_fit
center = (s + e - 1) / 2.0
seg_items.append({
's': s, 'e': e, 'l': l, 'mr': mr, 'ms': ms,
'score': float(score), 'center': float(center)
})

# 选择最多 3 段,按分数从高到低,避免重叠
seg_items.sort(key=lambda d: d['score'], reverse=True)
selected = []
for it in seg_items:
if len(selected) >= 3:
break
overlap = any(not (it['e'] <= jt['s'] or it['s'] >= jt['e']) for jt in selected)
if not overlap:
selected.append(it)

selected.sort(key=lambda d: d['center'])
stripe_count = len(selected)
if stripe_count == 0:
return 0.0, 0, 1.0, 0.0

rows_total = sum(d['l'] for d in selected)
rows_ratio = rows_total / float(H)
avg_dark_ratio = float(np.mean([d['mr'] for d in selected])) # 越低越暗(理想≈0.8)

# 均匀性:三条条纹中心之间的间距接近
centers = [d['center'] / float(H) for d in selected] # 归一化到 [0,1]
if len(centers) == 3:
gaps = np.diff(np.asarray(centers))
g1, g2 = float(gaps[0]), float(gaps[1])
uniformity = max(0.0, 1.0 - abs(g1 - g2) / (max(g1, g2, 1e-6)))
elif len(centers) == 2:
uniformity = 1.0
else:
uniformity = 0.0

return float(rows_ratio), int(stripe_count), float(avg_dark_ratio), float(uniformity)


def detect(args, model) -> list:
"""
仅使用“黑条检测算法”进行判定,完全不考虑模型输出。
返回:[[文件名, 0/1], ...],0-干净,1-存在三条横向黑色条纹
"""
results = []
try:
data_dir = os.path.abspath(args.poisoned_data_path)
if not os.path.isdir(data_dir):
raise FileNotFoundError(f"数据集目录不存在: {data_dir}")

# 递归收集图像文件,CSV 输出 basename 以适配评分
exts = {'.bmp', '.png', '.jpg', '.jpeg'}
files = []
for root, _, filenames in os.walk(data_dir):
for fn in filenames:
if os.path.splitext(fn)[1].lower() in exts:
rel = os.path.relpath(os.path.join(root, fn), data_dir)
rel = rel.replace(os.sep, '/')
files.append(rel)
files = sorted(files)
if len(files) == 0:
logger.warning(f"目录 {data_dir} 未找到图像文件(支持扩展名: {exts}),返回空结果。")
return []

# 判定阈值(固定在代码里,不新增命令行参数)
MIN_STRIPES = 3 # 至少 3 条
REQUIRE_EXACT_THREE = False # 是否必须恰好 3 条
UNIFORMITY_THR = 0.6 # 均匀性阈值
AVG_DARK_RATIO_MAX = 0.91 # 变暗比上限(越小越暗,~0.8 理想)
ROWS_RATIO_MIN = 0.01 # 条纹占比下限
ROWS_RATIO_MAX = 0.15 # 条纹占比上限

logger.info(f"开始仅基于黑条检测进行判定,共 {len(files)} 张图片")
for rel in tqdm(files, desc="StripeDetect", ncols=80):
fpath = os.path.join(data_dir, rel)
try:
im = Image.open(fpath).convert('RGB')
except Exception:
logger.warning(f"无法读取图像文件: {fpath},跳过。")
continue

rows_ratio, stripe_cnt, avg_dark_ratio, uniformity = _three_black_stripes_features(im)

# 判定规则(仅黑条):
# - 条纹数量:>= 3(或要求恰好 3)
# - 均匀性:uniformity >= UNIFORMITY_THR
# - 变暗强度:avg_dark_ratio <= AVG_DARK_RATIO_MAX(越小越暗,~0.8 理想)
# - 占比范围:ROWS_RATIO_MIN <= rows_ratio <= ROWS_RATIO_MAX
if REQUIRE_EXACT_THREE:
cnt_ok = (stripe_cnt == 3)
else:
cnt_ok = (stripe_cnt >= MIN_STRIPES)

uni_ok = (uniformity >= UNIFORMITY_THR)
dark_ok = (avg_dark_ratio <= AVG_DARK_RATIO_MAX)
ratio_ok = (ROWS_RATIO_MIN <= rows_ratio <= ROWS_RATIO_MAX)

# is_poison = int(cnt_ok and uni_ok and dark_ok and ratio_ok)
is_poison = int(ratio_ok)

results.append([path.basename(rel), is_poison])

logger.info("黑条检测完成。")
return results

except Exception:
logger.error("检测流程发生异常:\n" + traceback.format_exc())
raise

最后得分大概是85分

深度伪造

要求:

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
比赛任务

在本任务中,要求参赛者完善深伪检测算法,检测出所有图像中被伪造的图像。

任务要求:补全函数

数据集介绍

特别说明:参赛选手不允许使用额外数据

本次靶场使用自定义数据集,该数据集是200张人脸图片

数据集格式如下:

数据集总量为200,数据集结构示例如下:

├── data

├── 000001.jpg
├── 000002.jpg
├──...
└── 0000200.jpg


评价指标

在对数据伪造后,会输出一个 csv 文件,文件中记录了所有图像的文件名以及是否被伪造。未伪造标记为0,伪造图像标记为1。

在对数据检测后,也会输出一个 csv 文件,文件中记录了所有图像的文件名以及检测出的是否被伪造。检测结果中的未伪造图像标记为0,伪造图像标记为1。

csv文件中包括两列(不包含表头),第一列为文件名,第二列为检测结果,内容示例如下:

001.jpg 0

002.jpg 1

会根据伪造结果和检测结果,计算出选手的TP(True Positive)、TN(True Negative),并计算得到选手的最终准确率。

哎,这个是真不会啊。。。。。。。。

确实不太熟悉CV,这场比赛一大败笔

第一个想法就是直接交给ai生成,没想到这个是最高分了。。

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
def features(self, data_dict: dict) -> torch.Tensor:
# Extract input tensor from data_dict with common key fallbacks
x = None
for k in ['image', 'img', 'images', 'input', 'inputs', 'frame']:
if k in data_dict:
x = data_dict[k]
break
if x is None:
raise KeyError("Input image tensor not found in data_dict. Expected one of keys: ['image', 'img', 'images', 'input', 'inputs', 'frame']")

# Ensure tensor is on the same device as the model
device = next(self.parameters()).device
x = x.to(device, non_blocking=True)

# Forward through backbone to obtain feature representation
out = self.backbone(x)
return out


def classifier(self, features: torch.Tensor) -> torch.Tensor:
# Normalize various possible backbone outputs to a 2D feature tensor [B, D]
def pick_tensor_from_container(container):
# Prefer common semantic keys if it's a dict
if isinstance(container, dict):
for key in ['feat', 'features', 'embedding', 'pool', 'pooled', 'backbone_feat']:
if key in container and torch.is_tensor(container[key]):
return container[key]
# Fallback: first tensor value
for v in container.values():
if torch.is_tensor(v):
return v
raise ValueError("Backbone dict output does not contain tensor values.")
# If it's a list/tuple, pick the last tensor by default
if isinstance(container, (list, tuple)):
for item in reversed(container):
if torch.is_tensor(item):
return item
raise ValueError("Backbone list/tuple output does not contain tensor values.")
# Otherwise assume it's already a tensor
if torch.is_tensor(container):
return container
raise TypeError(f"Unsupported backbone output type: {type(container)}")

feat = pick_tensor_from_container(features)

# If the backbone already returned logits (heuristic), pass through
if feat.ndim == 2 and feat.size(-1) == 2 and not hasattr(self, 'cls_head'):
return feat

# Reduce to [B, D]
if feat.ndim == 4:
# [B, C, H, W] -> [B, C]
feat_vec = F.adaptive_avg_pool2d(feat, 1).flatten(1)
elif feat.ndim == 3:
# [B, C, T] or similar -> [B, C]
feat_vec = F.adaptive_avg_pool1d(feat, 1).squeeze(-1)
elif feat.ndim == 2:
feat_vec = feat
else:
# Fallback: flatten all but batch
feat_vec = feat.view(feat.size(0), -1)

in_dim = feat_vec.size(-1)
device = feat_vec.device

# Build or update classification head dynamically to match feature dim
if not hasattr(self, 'cls_head') or not isinstance(self.cls_head, nn.Linear) or self.cls_head.in_features != in_dim:
self.cls_head = nn.Linear(in_dim, 2)
self.cls_head.to(device)

logits = self.cls_head(feat_vec)
return logits

第二个,直接根据源码里面没删的注释,找到了github上的源码:

1
2
3
4
5
6
7
8
9
10
Evaluates a folder of video files or a single file with a xception binary
classification network.

Usage:
python detect_from_video.py
-i <folder with video files or path to video file>
-m <path to model file>
-o <path to output folder, will write one or multiple output videos there>

Author: Xiaotian Si

image-20250826003500338

不过,问题是,题目是个缝合怪 但还是用这个项目的代码交了一下:

1
2
3
4
5
6
def features(self, data_dict: dict) -> torch.tensor:
x = self.backbone.features(data_dict['image'])
return x

def classifier(self, features: torch.tensor) -> torch.tensor:
return self.backbone.classifier(features)

第三个就是根据一个攻击代码,喂给gpt,得到了一个检测代码,不过这个效果太差

唉唉唉,总结一下,还不错,学姐太有实力。

不过tm霸榜1个多小时,最后5分钟把我们超了……南平了。差了几分,还是学的不够多