不可见水印移除 — 扩散再生攻击¶
核心论文:"Image Watermarks Are Removable Using Controllable Regeneration from Clean Noise" (ICLR 2025)
特点:需要 GPU(CUDA / MPS),首次运行下载 ~12GB 模型
设计思路¶
不可见水印(SynthID、StableSignature、TreeRing 等)嵌入在图像的频域/潜空间中,对裁剪、缩放、JPEG 压缩有鲁棒性。传统的信号处理方法(滤波、去噪)难以完全去除。
扩散再生攻击的核心思想:不试图"找到并删除"水印信号,而是将图像作为输入,通过扩散模型"重新生成"一张视觉上相同但潜空间不同的图像。水印在再生过程中被自然破坏。
两条管线¶
| 管线 | 基础模型 | 分辨率 | 特点 |
|---|---|---|---|
| Default (SDXL) | stabilityai/stable-diffusion-xl-base-1.0 |
1024px | 默认,实证击败 SynthID v2 |
| CtrlRegen | SG161222/Realistic_Vision_V4.0 + ControlNet + DINOv2 |
512px | ICLR 2025 论文方法,更精细 |
Default 管线(SDXL)¶
流程¶
关键实现(img2img_runner.py)¶
pipeline(
prompt="", # 空提示!不引入文本语义
image=image, # 输入图像
strength=strength, # 去噪强度(默认 0.04)
num_inference_steps=50, # 总步数
guidance_scale=7.5, # CFG 引导
generator=generator, # 固定种子可复现
)
关键设计决策:
- 空 prompt
"":不引入额外文本语义,仅靠图像本身的 latent 进行再生。避免生成内容偏移 - 极低 strength 0.04:
effective_steps = max(1, int(50 × 0.04)) = 2——实际只做 2 步去噪。这意味着对图像的扰动极小,视觉几乎无损,但足以破坏水印的频域模式 - guidance_scale 7.5:标准的 CFG 值
设备自动选择¶
def get_device():
if torch.cuda.is_available():
return "cuda"
elif torch.backends.mps.is_available():
return "mps"
return "cpu"
CUDA 环境下自动使用 float16,CPU/MPS 使用 float32。
CUDA 自动修复¶
当检测到 NVIDIA GPU 存在但 torch.cuda 不可用时,WatermarkRemover 会:
- 从
nvidia-smi解析 CUDA 版本 pip install对应版本的 PyTorchos.execl()重启当前进程- 环境变量
NOAI_CUDA_FIXED防止无限循环
CtrlRegen 管线(ICLR 2025)¶
架构¶
graph TD
IMG[原图] --> CANNY[Canny 边缘检测]
IMG --> DINO[DINOv2 图像编码]
CANNY --> CN[Spatial ControlNet]
DINO --> IPA[IP-Adapter]
CN --> SD[SD1.5 Img2Img Pipeline]
IPA --> SD
SD --> OUT[再生图像]
IMG --> COLOR[色彩匹配]
OUT --> COLOR
COLOR --> FINAL[最终输出]
模型加载(ctrlregen/engine.py)¶
共加载 5 个模型组件:
| 组件 | 模型 ID | 用途 |
|---|---|---|
| Spatial ControlNet | yepengliu/ctrlregen/spatialnet_ckp/ |
Canny 边缘→空间结构控制 |
| 基础 SD 模型 | SG161222/Realistic_Vision_V4.0_noVAE |
生成骨干 |
| 自定义 VAE | stabilityai/sd-vae-ft-mse |
编解码 |
| DINOv2 编码器 | facebook/dinov2-giant |
语义特征提取(替代 CLIP) |
| IP-Adapter 权重 | yepengliu/ctrlregen/semanticnet_ckp/ |
语义注入 UNet |
自定义管线(pipeline.py)¶
class CustomCtrlRegenPipeline(
StableDiffusionControlNetImg2ImgPipeline,
CustomIPAdapterMixin
):
pass
Python MRO 保证:标准方法从 diffusers 管线解析,load_ctrlregen_ip_adapter 从 mixin 添加。
IP-Adapter 实现(ip_adapter.py)¶
- 替换标准 CLIP 为
facebook/dinov2-giant(ViT-Giant,1.1GB) - 权重格式:
image_proj(投影层) +ip_adapter(注入 UNet 的 cross-attention) - 通过
unet._load_ip_adapter_weights(state_dicts)加载 - 支持
.safetensors和.bin格式
单图处理流程¶
# 1. Canny 边缘提取
edges = CannyDetector(image, low_threshold=100, high_threshold=150)
# 2. 管线调用
pipeline(
prompt="best quality, high quality",
negative_prompt="monochrome, lowres, bad anatomy, worst quality, low quality",
image=image,
control_image=edges,
controlnet_conditioning_scale=1.0,
control_guidance_start=0.0,
control_guidance_end=1.0,
ip_adapter_image=[image], # 原图作为语义参考
guidance_scale=2.0, # 比 SDXL 的 7.5 低很多
strength=0.04,
)
分块处理(tiling.py)¶
大图(>512px)分块处理避免显存溢出:
tile_size = 512
overlap = 192
# 余弦渐变混合权重
def make_blend_weight(h, w, overlap):
# 中心 = 1.0,边缘 = cosine ramp
weight = np.ones((h, w), dtype=np.float32)
for i in range(overlap):
alpha = 0.5 * (1 + np.cos(np.pi * i / overlap))
weight[i, :] *= alpha
weight[h-1-i, :] *= alpha
weight[:, i] *= alpha
weight[:, w-1-i] *= alpha
return weight
每个 tile 处理后加权累加到 canvas,最后除以权重总和归一化。
色彩匹配(color.py)¶
扩散再生会偏移色彩,用 color-matcher 库校正:
from color_matcher import ColorMatcher
cm = ColorMatcher()
matched = cm.transfer(src=regen_img, ref=original_img, method='hm-mkl-hm')
hm-mkl-hm = 两遍直方图匹配 + MKL 传输。
InvisibleEngine 外层编排¶
invisible_engine.py 将上述所有组件串联:
graph TD
A[输入图像] --> B[EXIF 旋转修正]
B --> C[下采样到 1024px]
C --> D[保存临时文件]
D --> E[Phase 1: YOLO 人脸提取]
E --> F[WatermarkRemover 移除水印]
F --> G[Phase 2: 人脸恢复]
G --> H{humanize > 0?}
H -->|是| I[Analog Humanizer]
H -->|否| J[跳过]
I --> K[上采样回原始分辨率]
J --> K
K --> L[输出清洁图像]
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
strength |
0.04 | 去噪强度 |
num_inference_steps |
50 | 总扩散步数 |
guidance_scale |
7.5 (SDXL) / 2.0 (CtrlRegen) | CFG 引导强度 |
humanize |
0.0 | 胶片噪声强度(0=不启用) |
强度预设(watermark_profiles.py)¶
| 水印类型 | Strength | 说明 |
|---|---|---|
| StableSignature | 0.04 | 低扰动 |
| DwtEctSVD | 0.04 | 低扰动 |
| RivaGAN | 0.04 | 低扰动 |
| SSL | 0.04 | 低扰动 |
| Hidden | 0.04 | 低扰动 |
| 通用默认 | 0.35 | 中等 |
| StegaStamp | 0.7 | 高扰动 |
| TreeRing | 0.7 | 高扰动 |
| RingID | 0.7 | 高扰动 |