跳转至

可见水印移除 — GeminiEngine

文件gemini_engine.py(548 行)

特点:纯 CPU 运算,~0.05s/张,无 ML 依赖

核心原理:Alpha 混合反转

Google Gemini(内部代号 Nano Banana)将闪光 logo 通过 alpha 混合叠加到图像上:

\[ \text{watermarked} = \alpha \times \text{logo} + (1 - \alpha) \times \text{original} \]

已知 αlogo(白色,值为 255),反转即可恢复原始图像:

\[ \text{original} = \frac{\text{watermarked} - \alpha \times 255}{1 - \alpha} \]

实现细节(_reverse_alpha_blend()

def _reverse_alpha_blend(self, image, alpha_map, position):
    logo_value = 255.0
    alpha_threshold = 0.002  # 忽略极低 alpha
    max_alpha = 0.99         # 防止除零

    alpha_3d = alpha_map[:, :, np.newaxis]  # 广播到 3 通道
    mask = alpha_map >= alpha_threshold

    result = image.astype(np.float64)
    result[mask] = (result[mask] - alpha_3d[mask] * logo_value) / (1.0 - alpha_3d[mask])
    return np.clip(result, 0, 255).astype(np.uint8)

关键设计:

  • alpha_threshold = 0.002:低于此值的像素完全不处理,避免引入噪声
  • max_alpha = 0.99:防止 (1 - α) 为零导致除法爆炸
  • numpy 广播 alpha[:, :, np.newaxis]:一次性处理三通道,向量化高效计算
  • 仅在 alpha >= threshold 的 mask 区域应用反转,其余保持原样

Alpha Map 生成

从纯黑背景捕获(_calculate_alpha_map()

在纯黑背景(RGB=0)上捕获 Gemini 水印输出,alpha 计算:

\[ \alpha = \frac{\max(R, G, B)}{255} \]

取三通道最大值作为亮度代表——因为黑色背景下 watermarked = α × logo,logo 为白色。

项目内嵌两套 alpha map:

  • assets/gemini_bg_48.png — 48×48(用于小图)
  • assets/gemini_bg_96.png — 96×96(用于大图)

动态缩放(get_interpolated_alpha()

以 96×96 高分辨率 alpha map 为源,根据目标尺寸动态缩放:

if size > 96:
    return cv2.resize(alpha_96, (size, size), interpolation=cv2.INTER_LINEAR)
else:
    return cv2.resize(alpha_96, (size, size), interpolation=cv2.INTER_AREA)

3 阶段 NCC 检测器

detect_watermark(image) 方法实现位置和尺度的自动检测。

阶段 1:多尺度空间 NCC 搜索

在图像右下角 256×256 区域内搜索水印位置:

search_region = gray[h-256:h, w-256:w]
for scale in range(16, 120, 2):  # 步长 2
    tmpl = cv2.resize(source_alpha, (scale, scale))
    result = cv2.matchTemplate(search_region, tmpl, cv2.TM_CCOEFF_NORMED)
    _, max_val, _, max_loc = cv2.minMaxLoc(result)

    # 尺度校正权重:抑制小尺度 NCC 偏差
    weight = min(1.0, (scale / 96.0) ** 0.5)
    adj_val = max_val * weight

遍历 52 个尺度(16, 18, 20, ... , 118),取最佳 adj_val

阶段 2:梯度 NCC

对匹配区域和 alpha map 分别计算 Sobel 梯度:

gx = cv2.Sobel(region, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(region, cv2.CV_32F, 0, 1, ksize=3)
gradient = cv2.magnitude(gx, gy)
gradient_score = cv2.matchTemplate(gradient, tmpl_gradient, cv2.TM_CCOEFF_NORMED)

梯度图对边缘更敏感,能验证水印的结构特征。

阶段 3:方差分析

选取水印区域正上方等高的参考区域,比较方差:

_, watermark_std = cv2.meanStdDev(gray_watermark_region)
_, reference_std = cv2.meanStdDev(gray_reference_region)
var_score = max(0, min(1, 1 - watermark_std / reference_std))

水印区域通常比正常图像区域更"平坦"(方差更低)。

置信度融合

\[ \text{confidence} = \text{spatial} \times 0.50 + \text{gradient} \times 0.30 + \text{variance} \times 0.20 \]

检测阈值:0.35

水印位置配置(WatermarkPosition

条件 Logo 尺寸 边距
图片 > 1024×1024 96×96 (64, 64)
其他 48×48 (32, 32)

位置公式:

\[ x = \text{width} - \text{margin\_right} - \text{logo\_size} \]
\[ y = \text{height} - \text{margin\_bottom} - \text{logo\_size} \]

即水印固定在右下角

Inpainting 清理

alpha 反转后会有残留的边缘 artifact,通过 inpainting 修复。

Mask 生成

# 从 alpha map 计算梯度幅值
gx = cv2.Sobel(alpha_map, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(alpha_map, cv2.CV_32F, 0, 1, ksize=3)
gradient = cv2.magnitude(gx, gy)

# 归一化 + gamma 校正(√)
normalized = cv2.normalize(gradient, None, 0, 255, cv2.NORM_MINMAX)
gamma_corrected = np.sqrt(normalized / 255.0) * 255.0

# 膨胀
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.dilate(gamma_corrected, kernel)

三种修复方法

方法 实现 适用场景
gaussian 高斯模糊 + 权重混合 默认,轻微残留
telea cv2.inpaint(TELEA) 边缘较明显
ns cv2.inpaint(NS) 纹理一致性要求高
blended = roi * (1 - weight) + repaired * weight
# weight = 0.85, inpaint_radius = 10, padding = 32

二值化阈值 30(uint8),仅在 mask 白色区域执行 inpainting。

完整流程

graph LR
    A[输入图像] --> B[灰度化]
    B --> C[3 阶段 NCC 检测]
    C --> D{置信度 ≥ 0.35?}
    D -->|否| E[返回原图]
    D -->|是| F[计算 alpha map]
    F --> G[Alpha 混合反转]
    G --> H[梯度 mask 生成]
    H --> I[Inpainting 清理]
    I --> J[输出清洁图像]