@pilio/gemini-watermark-remover 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -23
- package/README_zh.md +14 -0
- package/package.json +1 -1
- package/src/core/allenkFdncnnDenoise.js +44 -0
- package/src/core/geminiSizeCatalog.js +64 -31
- package/src/core/watermarkProcessor.js +269 -9
package/README.md
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
[中文文档](README_zh.md)
|
|
2
|
-
|
|
3
|
-
> 🔥 Tired of Gemini watermarks? Try the more powerful **GPT Image 2** at [pilio.ai](https://pilio.ai) — free for a limited time.
|
|
4
|
-
|
|
5
|
-
# Gemini Watermark Remover — Lossless Watermark Removal Tool
|
|
6
|
-
|
|
7
|
-
An open-source tool to **remove Gemini watermarks** from AI-generated images with high-fidelity, reproducible results on supported outputs. Built with pure JavaScript, the engine uses a mathematically exact **Reverse Alpha Blending** algorithm instead of unpredictable AI inpainting.
|
|
8
|
-
|
|
9
|
-
> **🚀 Looking for the `Online Gemini Watermark Remover (Recommended)`? Try [geminiwatermarkremover.io](https://geminiwatermarkremover.io/)** — free, no install, works directly in your browser.
|
|
10
|
-
>
|
|
11
|
-
> 💡 Got watermarks that Gemini Watermark Remover can't handle? Try the general-purpose AI watermark remover: [pilio.ai/image-watermark-remover](https://pilio.ai/image-watermark-remover)
|
|
12
|
-
|
|
13
|
-
<p align="center">
|
|
1
|
+
[中文文档](README_zh.md)
|
|
2
|
+
|
|
3
|
+
> 🔥 Tired of Gemini watermarks? Try the more powerful **GPT Image 2** at [pilio.ai](https://pilio.ai) — free for a limited time.
|
|
4
|
+
|
|
5
|
+
# Gemini Watermark Remover — Lossless Watermark Removal Tool
|
|
6
|
+
|
|
7
|
+
An open-source tool to **remove Gemini watermarks** from AI-generated images with high-fidelity, reproducible results on supported outputs. Built with pure JavaScript, the engine uses a mathematically exact **Reverse Alpha Blending** algorithm instead of unpredictable AI inpainting.
|
|
8
|
+
|
|
9
|
+
> **🚀 Looking for the `Online Gemini Watermark Remover (Recommended)`? Try [geminiwatermarkremover.io](https://geminiwatermarkremover.io/)** — free, no install, works directly in your browser.
|
|
10
|
+
>
|
|
11
|
+
> 💡 Got watermarks that Gemini Watermark Remover can't handle? Try the general-purpose AI watermark remover: [pilio.ai/image-watermark-remover](https://pilio.ai/image-watermark-remover)
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
14
|
<a href="https://geminiwatermarkremover.io/"><img src="https://img.shields.io/badge/🛠️_Online_Tool-geminiwatermarkremover.io-blue?style=for-the-badge" alt="Online Tool"></a>
|
|
15
|
+
<a href="https://geminiwatermarkremover.io/video"><img src="https://img.shields.io/badge/🎬_Video_Remover-New!-ff6b6b?style=for-the-badge" alt="Video Watermark Remover"></a>
|
|
15
16
|
<a href="https://chromewebstore.google.com/detail/gemini-watermark-remover/cjlmnfcfnofnglkphbcdclbpimdjkmdf"><img src="https://img.shields.io/badge/🧩_Chrome_Extension-Install-blueviolet?style=for-the-badge" alt="Chrome Extension"></a>
|
|
16
17
|
<a href="https://geminiwatermarkremover.io/userscript/gemini-watermark-remover.user.js"><img src="https://img.shields.io/badge/🐒_Userscript-Install-green?style=for-the-badge" alt="Userscript"></a>
|
|
17
18
|
<a href="https://pilio.ai/image-watermark-remover"><img src="https://img.shields.io/badge/🧹_General_Remover-pilio.ai-gray?style=for-the-badge" alt="General Watermark Remover"></a>
|
|
18
19
|
</p>
|
|
19
|
-
|
|
20
|
-
<p align="center">
|
|
21
|
-
<img src="https://count.getloli.com/@gemini-watermark-remover?name=gemini-watermark-remover&theme=minecraft&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=auto" width="400">
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
|
-
## Features
|
|
25
|
-
|
|
26
|
-
- ✅ **100% Local Processing** - All image processing happens locally in your browser or on your machine. Nothing is uploaded.
|
|
27
|
-
- ✅ **Mathematical Precision** - Based on the Reverse Alpha Blending formula, not "hallucinating" AI models.
|
|
28
|
-
- ✅ **Auto-Detection** - Automatically identifies watermark size and position using Gemini's known output catalog and local anchor search.
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://count.getloli.com/@gemini-watermark-remover?name=gemini-watermark-remover&theme=minecraft&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=auto" width="400">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- ✅ **100% Local Processing** - All image processing happens locally in your browser or on your machine. Nothing is uploaded.
|
|
28
|
+
- ✅ **Mathematical Precision** - Based on the Reverse Alpha Blending formula, not "hallucinating" AI models.
|
|
29
|
+
- ✅ **Auto-Detection** - Automatically identifies watermark size and position using Gemini's known output catalog and local anchor search.
|
|
29
30
|
- ✅ **Flexible Usage** - Online tool for quick use, Chrome extension or userscript for seamless Gemini page integration, CLI and Skill for scripting and automation.
|
|
31
|
+
- ✅ **Video Support** - Remove watermarks from Gemini-generated videos at [geminiwatermarkremover.io/video](https://geminiwatermarkremover.io/video).
|
|
30
32
|
- ✅ **Cross-Platform** - Works in modern browsers (Chrome, Firefox, Safari, Edge) and Node.js environments.
|
|
31
33
|
|
|
32
34
|
## Gemini Watermark Removal Examples
|
|
@@ -66,6 +68,17 @@ For all users — the fastest and easiest way to remove Gemini watermarks from i
|
|
|
66
68
|
3. The engine will automatically process and remove the watermark.
|
|
67
69
|
4. Download the cleaned image.
|
|
68
70
|
|
|
71
|
+
### Online Video Watermark Remover (New!)
|
|
72
|
+
|
|
73
|
+
For Gemini-generated **videos** with visible watermarks:
|
|
74
|
+
|
|
75
|
+
1. Open **[geminiwatermarkremover.io/video](https://geminiwatermarkremover.io/video)**.
|
|
76
|
+
2. Upload or drag-and-drop your Gemini-generated video.
|
|
77
|
+
3. The tool will automatically detect and remove the watermark from the video.
|
|
78
|
+
4. Download the cleaned video.
|
|
79
|
+
|
|
80
|
+
> **Note**: Video watermark removal runs entirely in your browser — no files are uploaded to any server.
|
|
81
|
+
|
|
69
82
|
### Chrome Extension
|
|
70
83
|
|
|
71
84
|
Use this if you do not want a userscript manager but still want automatic Gemini page integration for previews, copy, and download actions.
|
|
@@ -371,6 +384,7 @@ gemini-watermark-remover/
|
|
|
371
384
|
## Limitations
|
|
372
385
|
|
|
373
386
|
- Only removes **Gemini visible watermarks** <small>(the semi-transparent logo in bottom-right)</small>
|
|
387
|
+
- Supports both **image** and **video** watermark removal — video processing is available via the [online tool](https://geminiwatermarkremover.io/video)
|
|
374
388
|
- Does not remove invisible/steganographic watermarks. <small>[(Learn more about SynthID)](https://support.google.com/gemini/answer/16722517)</small>
|
|
375
389
|
- Designed for Gemini's current visible watermark pattern <small>(validated against this repo through April 2026)</small>
|
|
376
390
|
|
package/README_zh.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://geminiwatermarkremover.io/"><img src="https://img.shields.io/badge/🛠️_在线工具-geminiwatermarkremover.io-blue?style=for-the-badge" alt="在线工具"></a>
|
|
15
|
+
<a href="https://geminiwatermarkremover.io/video"><img src="https://img.shields.io/badge/🎬_视频去水印-New!-ff6b6b?style=for-the-badge" alt="视频去水印"></a>
|
|
15
16
|
<a href="https://chromewebstore.google.com/detail/gemini-watermark-remover/cjlmnfcfnofnglkphbcdclbpimdjkmdf"><img src="https://img.shields.io/badge/🧩_Chrome插件-安装-blueviolet?style=for-the-badge" alt="Chrome 插件"></a>
|
|
16
17
|
<a href="https://geminiwatermarkremover.io/userscript/gemini-watermark-remover.user.js"><img src="https://img.shields.io/badge/🐒_油猴脚本-安装-green?style=for-the-badge" alt="油猴脚本"></a>
|
|
17
18
|
<a href="https://pilio.ai/image-watermark-remover"><img src="https://img.shields.io/badge/🧹_通用去水印-pilio.ai-gray?style=for-the-badge" alt="通用去水印"></a>
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
- ✅ **数学精确** - 基于反向 Alpha 混合算法,而非 AI 修复模型
|
|
28
29
|
- ✅ **自动检测** - 基于 Gemini 已知输出尺寸目录和局部锚点搜索,自动识别水印大小与位置
|
|
29
30
|
- ✅ **灵活使用** - 在线工具快速上手、Chrome 插件或油猴脚本无缝集成 Gemini 页面、CLI 和 Skill 支持脚本化与自动化
|
|
31
|
+
- ✅ **视频支持** - 支持去除 Gemini 生成视频中的水印,访问 [geminiwatermarkremover.io/video](https://geminiwatermarkremover.io/video) 即可使用
|
|
30
32
|
- ✅ **跨平台** - 支持现代浏览器(Chrome、Firefox、Safari、Edge)和 Node.js 环境
|
|
31
33
|
|
|
32
34
|
## Gemini 去水印效果示例
|
|
@@ -66,6 +68,17 @@
|
|
|
66
68
|
3. 图片会自动开始处理,移除水印
|
|
67
69
|
4. 下载处理后的图片
|
|
68
70
|
|
|
71
|
+
### 在线视频去水印工具(新功能!)
|
|
72
|
+
|
|
73
|
+
针对带有可见水印的 Gemini 生成**视频**:
|
|
74
|
+
|
|
75
|
+
1. 浏览器打开 **[geminiwatermarkremover.io/video](https://geminiwatermarkremover.io/video)**
|
|
76
|
+
2. 拖拽或点击选择带水印的 Gemini 生成视频
|
|
77
|
+
3. 工具会自动检测并去除视频中的水印
|
|
78
|
+
4. 下载处理后的视频
|
|
79
|
+
|
|
80
|
+
> **说明**:视频去水印处理完全在浏览器本地完成,不会上传任何文件到服务器。
|
|
81
|
+
|
|
69
82
|
### Chrome 插件
|
|
70
83
|
|
|
71
84
|
适合不想安装油猴脚本、但希望在 Gemini 页面里实时自动处理预览图、复制和下载动作的用户。
|
|
@@ -371,6 +384,7 @@ gemini-watermark-remover/
|
|
|
371
384
|
## 局限性
|
|
372
385
|
|
|
373
386
|
- 只去除了 **Gemini 可见的水印**<small>(即右下角的半透明 Logo)</small>
|
|
387
|
+
- 同时支持**图片**和**视频**水印去除 — 视频处理请使用[在线工具](https://geminiwatermarkremover.io/video)
|
|
374
388
|
- 无法去除隐形或隐写水印。<small>[(了解更多关于 SynthID 的信息)](https://support.google.com/gemini/answer/16722517)</small>
|
|
375
389
|
- 针对 Gemini 当前的可见水印模式设计<small>(本仓库验证范围截至 2026 年 4 月)</small>
|
|
376
390
|
|
package/package.json
CHANGED
|
@@ -188,6 +188,49 @@ function calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding = A
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
function calculateAllenkRuntimeRoi({
|
|
192
|
+
imageWidth,
|
|
193
|
+
imageHeight,
|
|
194
|
+
region,
|
|
195
|
+
padding = ALLENK_FDNCNN_MODEL.defaultPadding,
|
|
196
|
+
targetWidth = null,
|
|
197
|
+
targetHeight = null
|
|
198
|
+
} = {}) {
|
|
199
|
+
const padded = calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding });
|
|
200
|
+
if (!padded) return null;
|
|
201
|
+
|
|
202
|
+
const safeTargetWidth = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : null;
|
|
203
|
+
const safeTargetHeight = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : null;
|
|
204
|
+
if (
|
|
205
|
+
!safeTargetWidth ||
|
|
206
|
+
!safeTargetHeight ||
|
|
207
|
+
imageWidth < safeTargetWidth ||
|
|
208
|
+
imageHeight < safeTargetHeight ||
|
|
209
|
+
padded.width > safeTargetWidth ||
|
|
210
|
+
padded.height > safeTargetHeight
|
|
211
|
+
) {
|
|
212
|
+
return padded;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const centerX = region.x + region.width / 2;
|
|
216
|
+
const centerY = region.y + region.height / 2;
|
|
217
|
+
const x = clamp(Math.round(centerX - safeTargetWidth / 2), 0, imageWidth - safeTargetWidth);
|
|
218
|
+
const y = clamp(Math.round(centerY - safeTargetHeight / 2), 0, imageHeight - safeTargetHeight);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
x,
|
|
222
|
+
y,
|
|
223
|
+
width: safeTargetWidth,
|
|
224
|
+
height: safeTargetHeight,
|
|
225
|
+
inner: {
|
|
226
|
+
x: clamp(Math.round(region.x - x), 0, safeTargetWidth),
|
|
227
|
+
y: clamp(Math.round(region.y - y), 0, safeTargetHeight),
|
|
228
|
+
width: clamp(Math.round(region.width), 0, safeTargetWidth),
|
|
229
|
+
height: clamp(Math.round(region.height), 0, safeTargetHeight)
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
function embedAllenkRoiWeights({ roiWeights, roiWidth, roiHeight, paddedRoi, blurSigma = 1 } = {}) {
|
|
192
235
|
if (!roiWeights || !paddedRoi?.inner || roiWidth <= 0 || roiHeight <= 0) {
|
|
193
236
|
return new Float32Array(0);
|
|
@@ -319,6 +362,7 @@ export {
|
|
|
319
362
|
blendAllenkDenoisedRoi,
|
|
320
363
|
buildAllenkFdncnnInput,
|
|
321
364
|
calculateAllenkPaddedRoi,
|
|
365
|
+
calculateAllenkRuntimeRoi,
|
|
322
366
|
convertAllenkFdncnnOutputToRgba,
|
|
323
367
|
createAllenkGradientMask,
|
|
324
368
|
embedAllenkRoiWeights,
|
|
@@ -240,6 +240,35 @@ function createV2SmallVariantConfig(width, height) {
|
|
|
240
240
|
return x >= 0 && y >= 0 ? config : null;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
function createProjectedConfig(baseConfig, scaleX, scaleY, { minLogoSize, maxLogoSize, roundLogoSize = Math.round }) {
|
|
244
|
+
if (!baseConfig) return null;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
logoSize: clamp(
|
|
248
|
+
roundLogoSize(baseConfig.logoSize * ((scaleX + scaleY) / 2)),
|
|
249
|
+
minLogoSize,
|
|
250
|
+
maxLogoSize
|
|
251
|
+
),
|
|
252
|
+
marginRight: Math.max(8, Math.round(baseConfig.marginRight * scaleX)),
|
|
253
|
+
marginBottom: Math.max(8, Math.round(baseConfig.marginBottom * scaleY)),
|
|
254
|
+
...(baseConfig.alphaVariant ? { alphaVariant: baseConfig.alphaVariant } : {})
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getNearOfficialProjectionConfigs(entry, baseConfig) {
|
|
259
|
+
const configs = [{ config: baseConfig, family: 'near-official-projected', source: `${entry.width}x${entry.height}` }];
|
|
260
|
+
if (entry?.modelFamily === 'gemini-3.x-image' && entry.resolutionTier === '1k') {
|
|
261
|
+
configs.push({
|
|
262
|
+
config: GEMINI_3X_CURRENT_1K_LARGE_MARGIN_WATERMARK_CONFIG,
|
|
263
|
+
family: 'near-official-current-large-margin',
|
|
264
|
+
source: `${entry.width}x${entry.height}-large-margin`,
|
|
265
|
+
roundLogoSize: Math.ceil
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return configs;
|
|
270
|
+
}
|
|
271
|
+
|
|
243
272
|
export function matchOfficialGeminiImageSize(width, height) {
|
|
244
273
|
const normalizedWidth = normalizeDimension(width);
|
|
245
274
|
const normalizedHeight = normalizeDimension(height);
|
|
@@ -389,9 +418,9 @@ export function resolveOfficialGeminiSearchConfigEntries(
|
|
|
389
418
|
// this only proposes search seeds; later validation still decides safety.
|
|
390
419
|
const targetAspectRatio = normalizedWidth / normalizedHeight;
|
|
391
420
|
const candidates = OFFICIAL_GEMINI_IMAGE_SIZES
|
|
392
|
-
.
|
|
421
|
+
.flatMap((entry) => {
|
|
393
422
|
const baseConfig = getEntryConfig(entry);
|
|
394
|
-
if (!baseConfig) return
|
|
423
|
+
if (!baseConfig) return [];
|
|
395
424
|
|
|
396
425
|
const scaleX = normalizedWidth / entry.width;
|
|
397
426
|
const scaleY = normalizedHeight / entry.height;
|
|
@@ -400,35 +429,39 @@ export function resolveOfficialGeminiSearchConfigEntries(
|
|
|
400
429
|
const relativeAspectRatioDelta = Math.abs(targetAspectRatio - entryAspectRatio) / entryAspectRatio;
|
|
401
430
|
const scaleMismatchRatio = Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY);
|
|
402
431
|
|
|
403
|
-
if (relativeAspectRatioDelta > maxRelativeAspectRatioDelta) return
|
|
404
|
-
if (scaleMismatchRatio > maxScaleMismatchRatio) return
|
|
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
|
+
if (relativeAspectRatioDelta > maxRelativeAspectRatioDelta) return [];
|
|
433
|
+
if (scaleMismatchRatio > maxScaleMismatchRatio) return [];
|
|
434
|
+
|
|
435
|
+
return getNearOfficialProjectionConfigs(entry, baseConfig)
|
|
436
|
+
.map((projection) => {
|
|
437
|
+
const config = createProjectedConfig(projection.config, scaleX, scaleY, {
|
|
438
|
+
minLogoSize,
|
|
439
|
+
maxLogoSize,
|
|
440
|
+
roundLogoSize: projection.roundLogoSize
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const x = normalizedWidth - config.marginRight - config.logoSize;
|
|
444
|
+
const y = normalizedHeight - config.marginBottom - config.logoSize;
|
|
445
|
+
if (x < 0 || y < 0) return null;
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
config,
|
|
449
|
+
metadata: {
|
|
450
|
+
family: projection.family,
|
|
451
|
+
sourcePriority: 4,
|
|
452
|
+
evidenceGate: 'required',
|
|
453
|
+
modelFamily: entry.modelFamily,
|
|
454
|
+
resolutionTier: entry.resolutionTier,
|
|
455
|
+
aspectRatio: entry.aspectRatio,
|
|
456
|
+
source: projection.source
|
|
457
|
+
},
|
|
458
|
+
score:
|
|
459
|
+
relativeAspectRatioDelta * 100 +
|
|
460
|
+
scaleMismatchRatio * 20 +
|
|
461
|
+
Math.abs(Math.log2(Math.max(scale, 1e-6)))
|
|
462
|
+
};
|
|
463
|
+
})
|
|
464
|
+
.filter(Boolean);
|
|
432
465
|
})
|
|
433
466
|
.filter(Boolean)
|
|
434
467
|
.sort((a, b) => a.score - b.score);
|
|
@@ -149,7 +149,11 @@ const WEAK_ALPHA_FINE_TUNE_MAX_GRADIENT_INCREASE = 0.08;
|
|
|
149
149
|
const STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_SPATIAL = 0.75;
|
|
150
150
|
const STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_GRADIENT = 0.65;
|
|
151
151
|
const STRONG_POSITIVE_FINE_TUNE_MIN_CURRENT_SPATIAL = 0.3;
|
|
152
|
+
const STRONG_POSITIVE_FINE_TUNE_MID_GAINS = Object.freeze([0.7, 0.85]);
|
|
152
153
|
const STRONG_POSITIVE_FINE_TUNE_EXTRA_GAINS = Object.freeze([0.95, 1]);
|
|
154
|
+
const STRONG_POSITIVE_FINE_TUNE_MAX_ACCEPTED_SPATIAL = 0.12;
|
|
155
|
+
const STRONG_POSITIVE_FINE_TUNE_MAX_ACCEPTED_GRADIENT = 0.25;
|
|
156
|
+
const STRONG_POSITIVE_FINE_TUNE_MAX_POSITIVE_HALO_LUM = 4;
|
|
153
157
|
const CATALOG_ALPHA_DARK_FINE_TUNE_MIN_ORIGINAL_SPATIAL = 0.6;
|
|
154
158
|
const CATALOG_ALPHA_DARK_FINE_TUNE_MIN_ORIGINAL_GRADIENT = 0.45;
|
|
155
159
|
const CATALOG_ALPHA_DARK_FINE_TUNE_MAX_NEGATIVE_RESIDUAL = -0.12;
|
|
@@ -164,6 +168,15 @@ const SMALL_PREVIEW_REFINEMENT_MIN_CURRENT_SPATIAL = 0.04;
|
|
|
164
168
|
const SMALL_PREVIEW_REFINEMENT_MAX_CURRENT_GRADIENT = 0.08;
|
|
165
169
|
const FIRST_PASS_SIGN_FLIP_GRADIENT_THRESHOLD = 0.08;
|
|
166
170
|
const FIRST_PASS_SIGN_FLIP_MIN_GRADIENT_DROP = 0.2;
|
|
171
|
+
const SMALL_ANCHOR_RELOCATION_MIN_SIZE = 40;
|
|
172
|
+
const SMALL_ANCHOR_RELOCATION_MAX_SIZE = 56;
|
|
173
|
+
const SMALL_ANCHOR_RELOCATION_SIZE_DELTA = 5;
|
|
174
|
+
const SMALL_ANCHOR_RELOCATION_MARGIN_DELTA = 10;
|
|
175
|
+
const SMALL_ANCHOR_RELOCATION_MIN_CURRENT_GRADIENT = 0.24;
|
|
176
|
+
const SMALL_ANCHOR_RELOCATION_MAX_ACCEPTED_SPATIAL = 0.14;
|
|
177
|
+
const SMALL_ANCHOR_RELOCATION_MAX_ACCEPTED_GRADIENT = 0.24;
|
|
178
|
+
const SMALL_ANCHOR_RELOCATION_MIN_ORIGINAL_SPATIAL = 0.32;
|
|
179
|
+
const SMALL_ANCHOR_RELOCATION_MIN_SUPPRESSION_GAIN = 0.22;
|
|
167
180
|
|
|
168
181
|
function nowMs() {
|
|
169
182
|
if (typeof globalThis.performance?.now === 'function') {
|
|
@@ -560,12 +573,16 @@ function fineTuneWeakPositiveResidualAlpha({
|
|
|
560
573
|
alphaGainCandidates.add(alphaGain);
|
|
561
574
|
}
|
|
562
575
|
|
|
563
|
-
|
|
576
|
+
const shouldTryStrongPositiveFineTune =
|
|
564
577
|
currentSpatialScore >= STRONG_POSITIVE_FINE_TUNE_MIN_CURRENT_SPATIAL &&
|
|
565
578
|
originalSpatialScore >= STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_SPATIAL &&
|
|
566
|
-
originalGradientScore >= STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_GRADIENT
|
|
567
|
-
|
|
568
|
-
|
|
579
|
+
originalGradientScore >= STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_GRADIENT;
|
|
580
|
+
|
|
581
|
+
if (shouldTryStrongPositiveFineTune) {
|
|
582
|
+
for (const alphaGain of [
|
|
583
|
+
...STRONG_POSITIVE_FINE_TUNE_MID_GAINS,
|
|
584
|
+
...STRONG_POSITIVE_FINE_TUNE_EXTRA_GAINS
|
|
585
|
+
]) {
|
|
569
586
|
alphaGainCandidates.add(alphaGain);
|
|
570
587
|
}
|
|
571
588
|
}
|
|
@@ -597,17 +614,22 @@ function fineTuneWeakPositiveResidualAlpha({
|
|
|
597
614
|
});
|
|
598
615
|
const absSpatialImprovement = Math.abs(currentSpatialScore) - Math.abs(spatialScore);
|
|
599
616
|
const gradientIncrease = gradientScore - currentGradientScore;
|
|
600
|
-
|
|
617
|
+
const strongPositiveClearsResidual = shouldTryStrongPositiveFineTune &&
|
|
618
|
+
Math.abs(spatialScore) <= STRONG_POSITIVE_FINE_TUNE_MAX_ACCEPTED_SPATIAL &&
|
|
619
|
+
gradientScore <= STRONG_POSITIVE_FINE_TUNE_MAX_ACCEPTED_GRADIENT &&
|
|
620
|
+
(artifacts?.halo?.positiveDeltaLum ?? Number.POSITIVE_INFINITY) <= STRONG_POSITIVE_FINE_TUNE_MAX_POSITIVE_HALO_LUM;
|
|
621
|
+
if (!strongPositiveClearsResidual && (
|
|
601
622
|
absSpatialImprovement < WEAK_ALPHA_FINE_TUNE_MIN_ABS_SPATIAL_IMPROVEMENT ||
|
|
602
623
|
gradientIncrease > WEAK_ALPHA_FINE_TUNE_MAX_GRADIENT_INCREASE
|
|
603
|
-
) {
|
|
624
|
+
)) {
|
|
604
625
|
continue;
|
|
605
626
|
}
|
|
606
627
|
|
|
607
628
|
const cost = artifacts
|
|
608
629
|
? artifacts.visualArtifactCost + Math.max(0, gradientIncrease) * 0.25
|
|
609
630
|
: Math.abs(spatialScore) + Math.max(0, gradientIncrease) * 0.25 + nearBlackRatio * 2;
|
|
610
|
-
|
|
631
|
+
const clearsResidual = strongPositiveClearsResidual;
|
|
632
|
+
if (!best || (clearsResidual && !best.clearsResidual) || (clearsResidual === best.clearsResidual && cost < best.cost)) {
|
|
611
633
|
best = {
|
|
612
634
|
imageData: candidate,
|
|
613
635
|
alphaGain,
|
|
@@ -615,7 +637,8 @@ function fineTuneWeakPositiveResidualAlpha({
|
|
|
615
637
|
gradientScore,
|
|
616
638
|
nearBlackRatio,
|
|
617
639
|
suppressionGain: originalSpatialScore - spatialScore,
|
|
618
|
-
cost
|
|
640
|
+
cost,
|
|
641
|
+
clearsResidual
|
|
619
642
|
};
|
|
620
643
|
}
|
|
621
644
|
}
|
|
@@ -1850,6 +1873,198 @@ function refineLocatedAggressiveRemoval({
|
|
|
1850
1873
|
return best;
|
|
1851
1874
|
}
|
|
1852
1875
|
|
|
1876
|
+
function shouldRelocateSmallFixedLocalAnchor({
|
|
1877
|
+
source,
|
|
1878
|
+
config,
|
|
1879
|
+
position,
|
|
1880
|
+
currentGradientScore,
|
|
1881
|
+
currentResidualVisibility
|
|
1882
|
+
}) {
|
|
1883
|
+
const sourceText = String(source || '');
|
|
1884
|
+
if (!sourceText.includes('fixed-local')) return false;
|
|
1885
|
+
if (
|
|
1886
|
+
!position ||
|
|
1887
|
+
position.width !== position.height ||
|
|
1888
|
+
position.width < SMALL_ANCHOR_RELOCATION_MIN_SIZE ||
|
|
1889
|
+
position.width > SMALL_ANCHOR_RELOCATION_MAX_SIZE
|
|
1890
|
+
) {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
if (
|
|
1894
|
+
!config ||
|
|
1895
|
+
config.logoSize < SMALL_ANCHOR_RELOCATION_MIN_SIZE ||
|
|
1896
|
+
config.logoSize > SMALL_ANCHOR_RELOCATION_MAX_SIZE
|
|
1897
|
+
) {
|
|
1898
|
+
return false;
|
|
1899
|
+
}
|
|
1900
|
+
if (!currentResidualVisibility?.visible) return false;
|
|
1901
|
+
return currentResidualVisibility.visiblePositiveHalo === true ||
|
|
1902
|
+
currentGradientScore >= SMALL_ANCHOR_RELOCATION_MIN_CURRENT_GRADIENT;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function resolveRelocationAlphaMap({ size, currentAlphaMap, currentPosition, alpha96, getAlphaMap }) {
|
|
1906
|
+
if (size === currentPosition?.width) return currentAlphaMap;
|
|
1907
|
+
if (typeof getAlphaMap === 'function') {
|
|
1908
|
+
const resolved = getAlphaMap(size);
|
|
1909
|
+
if (resolved) return resolved;
|
|
1910
|
+
}
|
|
1911
|
+
return interpolateAlphaMap(alpha96, 96, size);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function buildSmallAnchorRelocationCandidate({
|
|
1915
|
+
originalImageData,
|
|
1916
|
+
alphaMap,
|
|
1917
|
+
size,
|
|
1918
|
+
marginRight,
|
|
1919
|
+
marginBottom,
|
|
1920
|
+
alphaGain
|
|
1921
|
+
}) {
|
|
1922
|
+
const position = {
|
|
1923
|
+
x: originalImageData.width - marginRight - size,
|
|
1924
|
+
y: originalImageData.height - marginBottom - size,
|
|
1925
|
+
width: size,
|
|
1926
|
+
height: size
|
|
1927
|
+
};
|
|
1928
|
+
if (
|
|
1929
|
+
position.x < 0 ||
|
|
1930
|
+
position.y < 0 ||
|
|
1931
|
+
position.x + size > originalImageData.width ||
|
|
1932
|
+
position.y + size > originalImageData.height
|
|
1933
|
+
) {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
const originalSpatialScore = computeRegionSpatialCorrelation({
|
|
1938
|
+
imageData: originalImageData,
|
|
1939
|
+
alphaMap,
|
|
1940
|
+
region: { x: position.x, y: position.y, size }
|
|
1941
|
+
});
|
|
1942
|
+
if (originalSpatialScore < SMALL_ANCHOR_RELOCATION_MIN_ORIGINAL_SPATIAL) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
const originalGradientScore = computeRegionGradientCorrelation({
|
|
1947
|
+
imageData: originalImageData,
|
|
1948
|
+
alphaMap,
|
|
1949
|
+
region: { x: position.x, y: position.y, size }
|
|
1950
|
+
});
|
|
1951
|
+
const candidateImageData = cloneImageData(originalImageData);
|
|
1952
|
+
removeWatermark(candidateImageData, alphaMap, position, { alphaGain });
|
|
1953
|
+
const spatialScore = computeRegionSpatialCorrelation({
|
|
1954
|
+
imageData: candidateImageData,
|
|
1955
|
+
alphaMap,
|
|
1956
|
+
region: { x: position.x, y: position.y, size }
|
|
1957
|
+
});
|
|
1958
|
+
const gradientScore = computeRegionGradientCorrelation({
|
|
1959
|
+
imageData: candidateImageData,
|
|
1960
|
+
alphaMap,
|
|
1961
|
+
region: { x: position.x, y: position.y, size }
|
|
1962
|
+
});
|
|
1963
|
+
const suppressionGain = originalSpatialScore - spatialScore;
|
|
1964
|
+
if (
|
|
1965
|
+
Math.abs(spatialScore) > SMALL_ANCHOR_RELOCATION_MAX_ACCEPTED_SPATIAL ||
|
|
1966
|
+
gradientScore > SMALL_ANCHOR_RELOCATION_MAX_ACCEPTED_GRADIENT ||
|
|
1967
|
+
suppressionGain < SMALL_ANCHOR_RELOCATION_MIN_SUPPRESSION_GAIN
|
|
1968
|
+
) {
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
const residualVisibility = assessWatermarkResidualVisibility({
|
|
1973
|
+
imageData: candidateImageData,
|
|
1974
|
+
position,
|
|
1975
|
+
alphaMap
|
|
1976
|
+
});
|
|
1977
|
+
if (residualVisibility.visible) return null;
|
|
1978
|
+
|
|
1979
|
+
return {
|
|
1980
|
+
imageData: candidateImageData,
|
|
1981
|
+
alphaMap,
|
|
1982
|
+
position,
|
|
1983
|
+
config: { logoSize: size, marginRight, marginBottom },
|
|
1984
|
+
alphaGain,
|
|
1985
|
+
originalSpatialScore,
|
|
1986
|
+
originalGradientScore,
|
|
1987
|
+
spatialScore,
|
|
1988
|
+
gradientScore,
|
|
1989
|
+
suppressionGain,
|
|
1990
|
+
residualVisibility,
|
|
1991
|
+
cost: Math.abs(spatialScore) * 0.8 +
|
|
1992
|
+
Math.max(0, gradientScore) * 0.75 +
|
|
1993
|
+
Math.max(0, residualVisibility.positiveHaloLum ?? 0) * 0.01
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function refineSmallFixedLocalAnchorGeometry({
|
|
1998
|
+
originalImageData,
|
|
1999
|
+
currentAlphaMap,
|
|
2000
|
+
currentPosition,
|
|
2001
|
+
currentConfig,
|
|
2002
|
+
currentSource,
|
|
2003
|
+
currentGradientScore,
|
|
2004
|
+
currentResidualVisibility,
|
|
2005
|
+
alpha96,
|
|
2006
|
+
getAlphaMap
|
|
2007
|
+
}) {
|
|
2008
|
+
if (!shouldRelocateSmallFixedLocalAnchor({
|
|
2009
|
+
source: currentSource,
|
|
2010
|
+
config: currentConfig,
|
|
2011
|
+
position: currentPosition,
|
|
2012
|
+
currentGradientScore,
|
|
2013
|
+
currentResidualVisibility
|
|
2014
|
+
})) {
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const currentSize = currentConfig.logoSize ?? currentPosition.width;
|
|
2019
|
+
const currentMarginRight = currentConfig.marginRight;
|
|
2020
|
+
const currentMarginBottom = currentConfig.marginBottom;
|
|
2021
|
+
if (![currentSize, currentMarginRight, currentMarginBottom].every(Number.isFinite)) {
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
let best = null;
|
|
2026
|
+
const minSize = Math.max(SMALL_ANCHOR_RELOCATION_MIN_SIZE, currentSize);
|
|
2027
|
+
const maxSize = Math.min(SMALL_ANCHOR_RELOCATION_MAX_SIZE, currentSize + SMALL_ANCHOR_RELOCATION_SIZE_DELTA);
|
|
2028
|
+
const minMarginRight = Math.max(0, currentMarginRight - SMALL_ANCHOR_RELOCATION_MARGIN_DELTA);
|
|
2029
|
+
const maxMarginRight = currentMarginRight + SMALL_ANCHOR_RELOCATION_MARGIN_DELTA;
|
|
2030
|
+
const minMarginBottom = Math.max(0, currentMarginBottom - SMALL_ANCHOR_RELOCATION_MARGIN_DELTA);
|
|
2031
|
+
const maxMarginBottom = currentMarginBottom + SMALL_ANCHOR_RELOCATION_MARGIN_DELTA;
|
|
2032
|
+
|
|
2033
|
+
for (let size = minSize; size <= maxSize; size++) {
|
|
2034
|
+
const alphaMap = resolveRelocationAlphaMap({
|
|
2035
|
+
size,
|
|
2036
|
+
currentAlphaMap,
|
|
2037
|
+
currentPosition,
|
|
2038
|
+
alpha96,
|
|
2039
|
+
getAlphaMap
|
|
2040
|
+
});
|
|
2041
|
+
if (!alphaMap) continue;
|
|
2042
|
+
|
|
2043
|
+
for (let marginRight = minMarginRight; marginRight <= maxMarginRight; marginRight++) {
|
|
2044
|
+
for (let marginBottom = minMarginBottom; marginBottom <= maxMarginBottom; marginBottom++) {
|
|
2045
|
+
const candidateY = originalImageData.height - marginBottom - size;
|
|
2046
|
+
if (candidateY > currentPosition.y + 1) continue;
|
|
2047
|
+
for (const candidateAlphaGain of ALPHA_GAIN_CANDIDATES) {
|
|
2048
|
+
const candidate = buildSmallAnchorRelocationCandidate({
|
|
2049
|
+
originalImageData,
|
|
2050
|
+
alphaMap,
|
|
2051
|
+
size,
|
|
2052
|
+
marginRight,
|
|
2053
|
+
marginBottom,
|
|
2054
|
+
alphaGain: candidateAlphaGain
|
|
2055
|
+
});
|
|
2056
|
+
if (!candidate) continue;
|
|
2057
|
+
if (!best || candidate.cost < best.cost) {
|
|
2058
|
+
best = candidate;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
return best;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1853
2068
|
export function processWatermarkImageData(imageData, options = {}) {
|
|
1854
2069
|
const totalStartedAt = nowMs();
|
|
1855
2070
|
const debugTimingsEnabled = options.debugTimings === true;
|
|
@@ -2526,8 +2741,53 @@ export function processWatermarkImageData(imageData, options = {}) {
|
|
|
2526
2741
|
source = `${source}+small-preview-refine`;
|
|
2527
2742
|
}
|
|
2528
2743
|
|
|
2744
|
+
const smallFixedLocalResidualVisibility = assessWatermarkResidualVisibility({
|
|
2745
|
+
imageData: finalImageData,
|
|
2746
|
+
position,
|
|
2747
|
+
alphaMap
|
|
2748
|
+
});
|
|
2749
|
+
const smallFixedLocalRelocated = refineSmallFixedLocalAnchorGeometry({
|
|
2750
|
+
originalImageData,
|
|
2751
|
+
currentAlphaMap: alphaMap,
|
|
2752
|
+
currentPosition: position,
|
|
2753
|
+
currentConfig: config,
|
|
2754
|
+
currentSource: source,
|
|
2755
|
+
currentGradientScore: finalProcessedGradientScore,
|
|
2756
|
+
currentResidualVisibility: smallFixedLocalResidualVisibility,
|
|
2757
|
+
alpha96,
|
|
2758
|
+
getAlphaMap: options.getAlphaMap
|
|
2759
|
+
});
|
|
2760
|
+
if (smallFixedLocalRelocated) {
|
|
2761
|
+
recordAlphaAdjustmentStage({
|
|
2762
|
+
stage: 'small-fixed-local-anchor-relocation',
|
|
2763
|
+
fromAlphaGain: alphaGain,
|
|
2764
|
+
toAlphaGain: smallFixedLocalRelocated.alphaGain,
|
|
2765
|
+
beforeSpatialScore: finalProcessedSpatialScore,
|
|
2766
|
+
beforeGradientScore: finalProcessedGradientScore,
|
|
2767
|
+
afterSpatialScore: smallFixedLocalRelocated.spatialScore,
|
|
2768
|
+
afterGradientScore: smallFixedLocalRelocated.gradientScore,
|
|
2769
|
+
suppressionGain: smallFixedLocalRelocated.suppressionGain,
|
|
2770
|
+
cost: smallFixedLocalRelocated.cost,
|
|
2771
|
+
allowSameAlphaGain: true
|
|
2772
|
+
});
|
|
2773
|
+
finalImageData = smallFixedLocalRelocated.imageData;
|
|
2774
|
+
alphaMap = smallFixedLocalRelocated.alphaMap;
|
|
2775
|
+
position = smallFixedLocalRelocated.position;
|
|
2776
|
+
config = smallFixedLocalRelocated.config;
|
|
2777
|
+
alphaGain = smallFixedLocalRelocated.alphaGain;
|
|
2778
|
+
originalSpatialScore = smallFixedLocalRelocated.originalSpatialScore;
|
|
2779
|
+
originalGradientScore = smallFixedLocalRelocated.originalGradientScore;
|
|
2780
|
+
finalProcessedSpatialScore = smallFixedLocalRelocated.spatialScore;
|
|
2781
|
+
finalProcessedGradientScore = smallFixedLocalRelocated.gradientScore;
|
|
2782
|
+
suppressionGain = smallFixedLocalRelocated.suppressionGain;
|
|
2783
|
+
source = `${source}+small-anchor-relocated`;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2529
2786
|
const locatedAggressiveStartedAt = nowMs();
|
|
2530
|
-
if (
|
|
2787
|
+
if (
|
|
2788
|
+
options.locatedAggressiveRemoval !== false &&
|
|
2789
|
+
smallFixedLocalRelocated?.residualVisibility?.visible !== false
|
|
2790
|
+
) {
|
|
2531
2791
|
const aggressiveRefined = refineLocatedAggressiveRemoval({
|
|
2532
2792
|
originalImageData,
|
|
2533
2793
|
currentImageData: finalImageData,
|