可见水印移除 — 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) |
纹理一致性要求高 |
二值化阈值 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[输出清洁图像]