File size: 17,625 Bytes
d501783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685f135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d501783
 
 
 
 
 
 
 
685f135
 
d501783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685f135
 
 
 
 
 
 
 
 
 
 
 
 
 
d501783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685f135
 
d501783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685f135
 
d501783
 
 
 
 
 
 
685f135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d501783
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
"""
attacks.py

提供对检测模型(以 YOLOv8/ultralytics 为主)执行 FGSM 与 PGD 的实现。

设计思路与注意事项:
- 假定我们可以访问到底层的 torch.nn.Module(例如 ultralytics.YOLO 实例的 .model 成员)
  并能以 tensor 输入直接跑 forward(),得到原始预测张量 (batch, N_preds, C)
  其中通常 C = 5 + num_classes(bbox4 + obj_conf + class_logits)。
- 计算 loss: 对每个 anchor/pred,取 obj_conf * max_class_prob 作为该预测的置信度,
  把全局置信度求和作为被攻击的目标函数;对该目标函数**做最小化**以让检测置信下降。
  - FGSM: x_adv = x - eps * sign(grad(loss))
  - PGD: 多步迭代,每步做 x = x - alpha * sign(grad), 并投影到 L_inf 球体:|x-x_orig|<=eps
- 如果你的 ultralytics 版本/模型封装与假定不同,代码会抛错并提示如何修改。
"""

from typing import Tuple, Optional
import torch
import torch.nn as nn
import numpy as np
from PIL import Image
import torchvision.transforms as T
import math
import torch.nn.functional as F
from typing import Tuple, Dict

# ============= Resize image =====================
def _get_max_stride(net) -> int:
    s = getattr(net, "stride", None)
    if isinstance(s, torch.Tensor):
        return int(s.max().item())
    try:
        return int(max(s))
    except Exception:
        return 32  # 兜底

def letterbox_tensor(
    x: torch.Tensor,
    *,
    imgsz: int,
    stride: int,
    fill: float = 114.0 / 255.0,
    scaleup: bool = True
) -> Tuple[torch.Tensor, Dict]:
    """
    x: [1,3,H,W] in [0,1]
    返回: x_lb, meta (含缩放比例与左右上下 padding 以便反映射)
    """
    assert x.ndim == 4 and x.shape[0] == 1
    _, C, H, W = x.shape
    if imgsz is None:
        # 动态设定目标边长:把 max(H,W) 向上取整到 stride 的倍数(更贴近原生自动整形)
        imgsz = int(math.ceil(max(H, W) / stride) * stride)

    r = min(imgsz / H, imgsz / W)  # 等比缩放比例
    if not scaleup:
        r = min(r, 1.0)

    new_w = int(round(W * r))
    new_h = int(round(H * r))

    # 先等比缩放
    if (new_h, new_w) != (H, W):
        x = F.interpolate(x, size=(new_h, new_w), mode="bilinear", align_corners=False)

    # 再 padding 到 (imgsz, imgsz),保持与 YOLO 一致的对称填充
    dw = imgsz - new_w
    dh = imgsz - new_h
    left, right = dw // 2, dw - dw // 2
    top, bottom = dh // 2, dh - dh // 2

    x = F.pad(x, (left, right, top, bottom), mode="constant", value=fill)

    meta = {
        "ratio": r,
        "pad": (left, top),
        "resized_shape": (new_h, new_w),
        "imgsz": imgsz,
    }
    return x, meta

def unletterbox_to_original(
    x_lb: torch.Tensor, meta: Dict, orig_hw: Tuple[int, int]
) -> torch.Tensor:
    """
    把 letterboxed 张量([1,3,imgsz,imgsz])反映射回原始 H0,W0 尺寸(去 padding + 反缩放)
    """
    assert x_lb.ndim == 4 and x_lb.shape[0] == 1
    H0, W0 = orig_hw
    (left, top) = meta["pad"]
    (h_r, w_r) = meta["resized_shape"]

    # 去 padding(裁出等比缩放后的区域)
    x_unpad = x_lb[..., top:top + h_r, left:left + w_r]  # [1,3,h_r,w_r]

    # 反缩放到原图大小
    x_rec = F.interpolate(x_unpad, size=(H0, W0), mode="bilinear", align_corners=False)
    return x_rec

  
# ----- basic preprocessing / deprocessing (RGB PIL <-> torch tensor) -----
_to_tensor = T.Compose([
    T.ToTensor(),  # float in [0,1], shape C,H,W
])

_to_pil = T.ToPILImage()

def pil_to_tensor(img_pil: Image.Image, device: torch.device) -> torch.Tensor:
    """PIL RGB -> float tensor [1,3,H,W] on device"""
    t = _to_tensor(img_pil).unsqueeze(0).to(device)  # 1,C,H,W
    t.requires_grad = True
    return t

def tensor_to_pil(t: torch.Tensor) -> Image.Image:
    """tensor [1,3,H,W] (0..1) -> PIL RGB"""
    t = t.detach().cpu().squeeze(0).clamp(0.0, 1.0)
    return _to_pil(t)

# ----- helper to obtain underlying torch module from ultralytics YOLO wrapper -----
def get_torch_module_from_ultralytics(model) -> nn.Module:
    """
    Try to retrieve an nn.Module that accepts an input tensor and returns raw preds.
    For ultralytics.YOLO, .model is usually the underlying Detect/Model (nn.Module).
    """
    if hasattr(model, "model") and isinstance(model.model, nn.Module):
        return model.model
    # Some wrappers nest further; attempt a few common names
    for attr in ("model", "module", "net", "model_"):
        if hasattr(model, attr) and isinstance(getattr(model, attr), nn.Module):
            return getattr(model, attr)
    raise RuntimeError("无法找到底层 torch.nn.Module。请确保传入的是 ultralytics.YOLO 实例且能访问 model.model。")

# ----- interpret raw model outputs to confidences -----


def _ensure_bcn(preds):
    assert preds.ndim == 3
    B, C1, C2 = preds.shape
    if C1 - 4 > 0 and C2 >= 1000:   # [B, 4+nc, N]
        return preds
    if C2 - 4 > 0 and C1 >= 1000:   # [B, N, 4+nc]
        return preds.permute(0, 2, 1).contiguous()
    return preds

def _xywh_to_xyxy(xywh):
    x,y,w,h = xywh.unbind(-1)
    return torch.stack([x-w/2, y-h/2, x+w/2, y+h/2], dim=-1)

def _xyxy_to_xywh(xyxy):
    x1,y1,x2,y2 = xyxy.unbind(-1)
    cx = (x1+x2)/2; cy = (y1+y2)/2
    w  = (x2-x1).clamp(min=0); h = (y2-y1).clamp(min=0)
    return torch.stack([cx,cy,w,h], dim=-1)

def _map_xyxy_to_letterbox(xyxy_tensor, meta):
    if meta is None:
        return xyxy_tensor
    r = meta.get('ratio', meta.get('scale', (1.0, 1.0)))
    p = meta.get('pad', (0.0, 0.0))
    if isinstance(r, (int, float)):
        r = (float(r), float(r))
    rx, ry = float(r[0]), float(r[1])
    px, py = float(p[0]), float(p[1])
    x1 = xyxy_tensor[:, 0] * rx + px
    y1 = xyxy_tensor[:, 1] * ry + py
    x2 = xyxy_tensor[:, 2] * rx + px
    y2 = xyxy_tensor[:, 3] * ry + py
    return torch.stack([x1, y1, x2, y2], dim=-1)

def _iou_xyxy(b_xyxy, g_xyxy):
    N, M = b_xyxy.size(0), g_xyxy.size(0)
    b = b_xyxy[:, None, :].expand(N, M, 4)
    g = g_xyxy[None, :, :].expand(N, M, 4)
    inter_x1 = torch.maximum(b[...,0], g[...,0])
    inter_y1 = torch.maximum(b[...,1], g[...,1])
    inter_x2 = torch.minimum(b[...,2], g[...,2])
    inter_y2 = torch.minimum(b[...,3], g[...,3])
    inter_w  = (inter_x2 - inter_x1).clamp(min=0)
    inter_h  = (inter_y2 - inter_y1).clamp(min=0)
    inter    = inter_w * inter_h
    area_b   = (b[...,2]-b[...,0]).clamp(min=0) * (b[...,3]-b[...,1]).clamp(min=0)
    area_g   = (g[...,2]-g[...,0]).clamp(min=0) * (g[...,3]-g[...,1]).clamp(min=0)
    return inter / (area_b + area_g - inter + 1e-9)

def _gt_list_to_xyxy_tensor(gt_list, device, meta=None):
    if not gt_list:
        return torch.empty(0, 4, device=device, dtype=torch.float32)
    xyxy = torch.tensor([b['xyxy'] for b in gt_list], dtype=torch.float32, device=device)
    return _map_xyxy_to_letterbox(xyxy, meta)

def preds_to_targeted_loss(
    preds,                  # [B,4+nc,N] 或 [B,N,4+nc];类别部分最好是 logits
    target_cls: int,
    gt_xywh,                # 这里直接支持 list[{'xyxy':..., 'cls':..., 'conf':...}]
    topk: int = 20,
    kappa: float = 0.1,
    lambda_margin: float = 1.0,
    lambda_keep: float = 0.2,
    lambda_target: float = 0.0,    # 新增:恢复 -p_t.mean() 这项
    debug: bool = False,
    meta: dict | None = None,      # 若 GT 是原图坐标,传入 letterbox 的 meta
):
    preds = _ensure_bcn(preds)
    B, C, N = preds.shape
    nc = C - 4
    assert 0 <= target_cls < nc

    # 解析 GT(list -> tensor in letterbox coords)
    gt_xyxy_lb = _gt_list_to_xyxy_tensor(gt_xywh, preds.device, meta=meta)  # [M,4]

    boxes_bxn4 = preds[:, :4, :].permute(0, 2, 1)   # [B,N,4] (xywh, letterbox)
    logits_bxcn = preds[:, 4:, :]                   # [B,nc,N]

    # 若类别部分像概率(0~1),转为 logits
    zmin, zmax = logits_bxcn.min().item(), logits_bxcn.max().item()
    if 0.0 <= zmin and zmax <= 1.0:
        p = logits_bxcn.clamp(1e-6, 1-1e-6)
        logits_bxcn = torch.log(p) - torch.log1p(-p)

    # 选与 GT 最相关的候选 idx(batch=0)
    b_xyxy = _xywh_to_xyxy(boxes_bxn4[0])          # [N,4]
    if gt_xyxy_lb.numel() > 0:
        iou = _iou_xyxy(b_xyxy, gt_xyxy_lb)        # [N,M]
        best_per_gt = iou.argmax(dim=0)            # [M]
        idx = torch.unique(best_per_gt, sorted=False)
        if idx.numel() < topk:
            topvals = iou.max(dim=1).values
            topidx2 = torch.topk(topvals, k=min(topk, N)).indices
            idx = torch.unique(torch.cat([idx, topidx2], 0), sorted=False)[:topk]
    else:
        # 没 GT 就按当前最大类别置信度取 topk
        z = logits_bxcn[0]                         # [nc,N]
        pmax = z.softmax(dim=0).max(dim=0).values
        idx = torch.topk(pmax, k=min(topk, N)).indices

    if idx.numel() == 0:
        idx = torch.arange(min(topk, N), device=preds.device)

    # 取这些候选的类别 logits:[K,nc]
    z = logits_bxcn[0, :, idx].T                   # [K,nc]

    # 1) CW-style margin
    mask = torch.ones(nc, device=z.device, dtype=torch.bool)
    mask[target_cls] = False
    z_t   = z[:, target_cls]
    z_oth = z[:, mask].max(dim=1).values
    loss_margin = F.relu(kappa + z_oth - z_t).mean()

    # 2) keep(KL >= 0)
    with torch.no_grad():
        p_clean = z.detach().softmax(dim=1)
    logp_adv = z.log_softmax(dim=1)
    loss_keep = F.kl_div(logp_adv, p_clean, reduction="batchmean")

    # 3) 你的旧项:直接推高目标类 logit
    loss_target = -z_t.mean()

    loss = (
        lambda_margin * loss_margin
        + lambda_keep * loss_keep
        + lambda_target * loss_target
    )

    if debug:
        same_ratio = (z.argmax(dim=1) == target_cls).float().mean().item()
        print(
            f"[dbg] K={idx.numel()} nc={nc} target={target_cls} "
            f"margin={loss_margin.item():.6f} keep={loss_keep.item():.6f} "
            f"targ={loss_target.item():.6f} same_ratio={same_ratio:.3f} "
            f"z_t_mean={z_t.mean().item():.3f} z_oth_mean={z_oth.mean().item():.3f}"
        )
    return loss


# def preds_to_confidence_sum(preds: torch.Tensor) -> torch.Tensor:
#     """
#     preds: tensor shape (batch, N_preds, C) or (batch, C, H, W) depending on model.
#     We support the common YOLO format where last dim: [x,y,w,h,obj_conf, class_probs...]
#     Returns scalar: sum of (obj_conf * max_class_prob) over batch and predictions.
#     """
#     if preds is None:
#         raise ValueError("preds is None")
#     # handle shape (batch, N_preds, C)
#     if preds.ndim == 3:
#         # assume last dim: 5 + num_classes
#         if preds.shape[-1] < 6:
#             # can't interpret
#             raise RuntimeError(f"preds last dim too small ({preds.shape[-1]}). Expecting >=6.")
#         obj_conf = preds[..., 4]  # (batch, N)
#         class_probs = preds[..., 5:]  # (batch, N, num_cls)
#         max_class, _ = class_probs.max(dim=-1)  # (batch, N)
#         conf = obj_conf * max_class
#         return conf.sum()
#     # some models output (batch, C, H, W) - flatten
#     if preds.ndim == 4:
#         # try to collapse so that last dim is class
#         b, c, h, w = preds.shape
#         flat = preds.view(b, c, -1).permute(0, 2, 1)  # (batch, N, C)
#         return preds_to_confidence_sum(flat)
#     raise RuntimeError(f"Unhandled preds dimensionality: {preds.shape}")

# ----- core attack implementations -----
def fgsm_attack_on_detector(
    model,
    img_pil: Image.Image,
    eps: float = 0.03,
    device: Optional[torch.device] = None,
    imgsz: Optional[int] = None,  # None=自动对齐到 stride 倍数;也可传 640
    gt_xywh: torch.Tensor | None = None,  # letterbox坐标系下的目标框(可选)
    target_cls: int = 2,
) -> Image.Image:
    """
    Perform a single-step FGSM on a detection model (white-box).
    - model: ultralytics.YOLO wrapper (or anything where get_torch_module_from_ultralytics works)
    - img_pil: input PIL RGB
    - eps: max per-pixel perturbation in [0,1] (L_inf)
    Returns PIL image of adversarial example.
    """
    device = device or (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
    # get torch module
    net = get_torch_module_from_ultralytics(model)
    net = net.to(device).eval()
    for p in net.parameters():
      p.requires_grad_(False)   # 建议:避免对参数求梯度
    
    # (a) 原图 -> [1,3,H0,W0],随后先 detach 掉梯度
    x_orig = pil_to_tensor(img_pil, device)  
    H0, W0 = x_orig.shape[-2:]
    x_orig = x_orig.detach()
    
    # (b) 可微 letterbox
    s = _get_max_stride(net)
    x_lb, meta = letterbox_tensor(x_orig, imgsz=imgsz, stride=s, fill=114/255.0)
    x_lb = x_lb.clone().detach().requires_grad_(True)

    # (c) 前向与你的损失
    with torch.enable_grad():
        preds = net(x_lb)
        if isinstance(preds, (tuple, list)):
            tensor_pred = next((p for p in preds if isinstance(p, torch.Tensor) and p.ndim >= 3), None)
            if tensor_pred is None:
                raise RuntimeError("模型 forward 返回了 tuple/list,但无法从中找到预测张量。")
            preds = tensor_pred

        loss = - preds_to_targeted_loss(
            preds,
            target_cls=target_cls,
            gt_xywh=gt_xywh,    # 直接传你的 list[dict]
            topk=20,
            kappa=0.1,
            lambda_margin=1.0,
            lambda_keep=0.2,
            lambda_target=0.0,   # 恢复 -p_t.mean() 的影响
            debug=False,
            meta=meta            # 若 GT 是原图坐标,务必传 meta
        )

        # loss = - preds_to_confidence_sum(preds)  
        loss.backward()

    # (d) FGSM 在 letterboxed 空间施扰
    # FGSM update: x_adv = x + eps * sign(grad(loss wrt x))
    with torch.no_grad():
        adv_lb = (x_lb + eps * x_lb.grad.sign()).clamp(0, 1)

    # 清理(单步可选;PGD循环时必做)
    x_lb.grad = None
    net.zero_grad(set_to_none=True)

    # (e) 反映射回原图尺寸
    adv_orig = unletterbox_to_original(adv_lb, meta, (H0, W0)).detach()

    # (f) 转回 PIL
    adv_pil = tensor_to_pil(adv_orig)
    return adv_pil

def pgd_attack_on_detector(
    model,
    img_pil: Image.Image,
    eps: float = 0.03,      # L_inf 半径(输入在[0,1]域)
    alpha: float = 0.007,   # 步长
    iters: int = 10,
    device: Optional[torch.device] = None,
    imgsz: Optional[int] = None,  # None=自动对齐到 stride 倍数;也可传 640
    gt_xywh: torch.Tensor | None = None,  # letterbox坐标系下的目标框(可选)
    target_cls: int = 2,
):
    """
    在 YOLO 的 letterbox 域做 PGD,
    迭代结束后把对抗样本映回原图大小并返回 PIL。
    依赖你已实现的: pil_to_tensor, tensor_to_pil, letterbox_tensor, unletterbox_to_original, _get_max_stride
    """
    device = device or (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
    net = get_torch_module_from_ultralytics(model).to(device).eval()

    # 仅对输入求梯度,冻结参数以省资源
    for p in net.parameters():
        p.requires_grad_(False)

    # 原图 -> Tensor([1,3,H0,W0], [0,1])
    x0 = pil_to_tensor(img_pil, device).detach()
    H0, W0 = x0.shape[-2:]

    # 可微 letterbox(等比缩放 + 对称 pad 到 stride 倍数)
    s = _get_max_stride(net)
    x_lb_orig, meta = letterbox_tensor(x0, imgsz=imgsz, stride=s, fill=114/255.0)  # [1,3,S,S]
    x = x_lb_orig.clone().detach().requires_grad_(True)

    # targeted = True
    # sign = -1.0 if targeted else 1.0    # 定向取负号,非定向取正号
    for _ in range(iters):
        # 前向 + 反向(需要梯度)
        preds = net(x)
        if isinstance(preds, (tuple, list)):
            preds = next((p for p in preds if isinstance(p, torch.Tensor) and p.ndim >= 3), None)
            if preds is None:
                raise RuntimeError("模型 forward 返回 tuple/list,但未找到预测张量。")

        loss = - preds_to_targeted_loss(
            preds,
            target_cls=target_cls,
            gt_xywh=gt_xywh,    # 直接传你的 list[dict]
            topk=20,
            kappa=0.1,
            lambda_margin=1.0,
            lambda_keep=0.2,
            lambda_target=0.0,   # 恢复 -p_t.mean() 的影响
            debug=False,
            meta=meta            # 若 GT 是原图坐标,务必传 meta
        )
        
        # loss = - preds_to_confidence_sum(preds)      # 我们希望置信度总和下降 → 最小化
        loss.backward()

        # 更新步与投影(不记录计算图)
        with torch.no_grad():
            x.add_(alpha * x.grad.sign())
            # 投影到 L_inf 球: 通过裁剪 delta 更稳
            delta = (x - x_lb_orig).clamp(-eps, eps)
            x.copy_((x_lb_orig + delta).clamp(0.0, 1.0))

        # 清理并设置下一步
        x.grad = None
        net.zero_grad(set_to_none=True)
        x.requires_grad_(True)

    # 反映射回原图尺寸
    adv_orig = unletterbox_to_original(x.detach(), meta, (H0, W0)).detach()
    return tensor_to_pil(adv_orig)


# ----- graceful fallback / demo noise if whitebox impossible -----
def demo_random_perturbation(img_pil: Image.Image, eps: float = 0.03) -> Image.Image:
    """Non-gradient demo perturbation used as fallback."""
    arr = np.asarray(img_pil).astype(np.float32) / 255.0
    noise = np.sign(np.random.randn(*arr.shape)).astype(np.float32)
    adv = np.clip(arr + eps * noise, 0.0, 1.0)
    adv_img = Image.fromarray((adv * 255).astype(np.uint8))
    return adv_img