@pilio/gemini-watermark-remover 1.0.10
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/LICENSE +22 -0
- package/README.md +371 -0
- package/README_zh.md +371 -0
- package/bin/gwr.mjs +12 -0
- package/package.json +76 -0
- package/skills/gemini-watermark-remover/SKILL.md +28 -0
- package/skills/gemini-watermark-remover/agents/openai.yaml +3 -0
- package/skills/gemini-watermark-remover/references/inputs-and-outputs.md +9 -0
- package/skills/gemini-watermark-remover/references/limitations.md +5 -0
- package/skills/gemini-watermark-remover/references/usage.md +19 -0
- package/skills/gemini-watermark-remover/scripts/run.mjs +153 -0
- package/src/cli/gwrCli.js +17 -0
- package/src/cli/gwrRemoveCommand.js +317 -0
- package/src/core/adaptiveDetector.js +488 -0
- package/src/core/alphaMap.js +30 -0
- package/src/core/blendModes.js +70 -0
- package/src/core/candidateSelector.js +1446 -0
- package/src/core/canvasBlob.js +26 -0
- package/src/core/embeddedAlphaMaps.js +49 -0
- package/src/core/geminiSizeCatalog.js +239 -0
- package/src/core/multiPassRemoval.js +93 -0
- package/src/core/previewAlphaCalibration.js +822 -0
- package/src/core/restorationMetrics.js +251 -0
- package/src/core/selectionDebug.js +47 -0
- package/src/core/watermarkConfig.js +146 -0
- package/src/core/watermarkDecisionPolicy.js +163 -0
- package/src/core/watermarkDisplay.js +60 -0
- package/src/core/watermarkEngine.js +150 -0
- package/src/core/watermarkPresence.js +12 -0
- package/src/core/watermarkProcessor.js +875 -0
- package/src/core/workerClient.js +114 -0
- package/src/sdk/browser.d.ts +13 -0
- package/src/sdk/browser.js +29 -0
- package/src/sdk/image-data.d.ts +14 -0
- package/src/sdk/image-data.js +55 -0
- package/src/sdk/index.d.ts +144 -0
- package/src/sdk/index.js +8 -0
- package/src/sdk/node.d.ts +42 -0
- package/src/sdk/node.js +77 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { removeWatermark } from './blendModes.js';
|
|
2
|
+
import { removeRepeatedWatermarkLayers } from './multiPassRemoval.js';
|
|
3
|
+
import {
|
|
4
|
+
computeRegionGradientCorrelation,
|
|
5
|
+
computeRegionSpatialCorrelation,
|
|
6
|
+
warpAlphaMap
|
|
7
|
+
} from './adaptiveDetector.js';
|
|
8
|
+
import {
|
|
9
|
+
calculateNearBlackRatio,
|
|
10
|
+
scoreRegion,
|
|
11
|
+
selectInitialCandidate
|
|
12
|
+
} from './candidateSelector.js';
|
|
13
|
+
import { assessAlphaBandHalo } from './restorationMetrics.js';
|
|
14
|
+
import { createSelectionDebugSummary } from './selectionDebug.js';
|
|
15
|
+
import {
|
|
16
|
+
calculateWatermarkPosition,
|
|
17
|
+
detectWatermarkConfig,
|
|
18
|
+
resolveInitialStandardConfig
|
|
19
|
+
} from './watermarkConfig.js';
|
|
20
|
+
|
|
21
|
+
const RESIDUAL_RECALIBRATION_THRESHOLD = 0.5;
|
|
22
|
+
const MIN_SUPPRESSION_FOR_SKIP_RECALIBRATION = 0.18;
|
|
23
|
+
const MIN_RECALIBRATION_SCORE_DELTA = 0.18;
|
|
24
|
+
const MAX_NEAR_BLACK_RATIO_INCREASE = 0.05;
|
|
25
|
+
const OUTLINE_REFINEMENT_THRESHOLD = 0.42;
|
|
26
|
+
const OUTLINE_REFINEMENT_MIN_GAIN = 1.2;
|
|
27
|
+
const SUBPIXEL_REFINE_SHIFTS = [-0.25, 0, 0.25];
|
|
28
|
+
const SUBPIXEL_REFINE_SCALES = [0.99, 1, 1.01];
|
|
29
|
+
const ALPHA_GAIN_CANDIDATES = [1.05, 1.12, 1.2, 1.28, 1.36, 1.45, 1.52, 1.6, 1.7, 1.85, 2.0, 2.2, 2.4, 2.6];
|
|
30
|
+
const PREVIEW_EDGE_CLEANUP_MAX_SIZE = 40;
|
|
31
|
+
const PREVIEW_EDGE_CLEANUP_SPATIAL_THRESHOLD = 0.08;
|
|
32
|
+
const PREVIEW_EDGE_CLEANUP_GRADIENT_THRESHOLD = 0.1;
|
|
33
|
+
const PREVIEW_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT = 0.03;
|
|
34
|
+
const PREVIEW_EDGE_CLEANUP_MAX_SPATIAL_DRIFT = 0.04;
|
|
35
|
+
const PREVIEW_EDGE_CLEANUP_MAX_APPLIED_PASSES = 3;
|
|
36
|
+
const PREVIEW_EDGE_CLEANUP_FINE_GRADIENT_THRESHOLD = 0.16;
|
|
37
|
+
const PREVIEW_EDGE_CLEANUP_FINE_MIN_GRADIENT_IMPROVEMENT = 0.005;
|
|
38
|
+
const PREVIEW_EDGE_CLEANUP_HALO_RELAXED_MIN_GRADIENT_IMPROVEMENT = 0.01;
|
|
39
|
+
const PREVIEW_EDGE_CLEANUP_HALO_WEIGHT = 0.02;
|
|
40
|
+
const PREVIEW_EDGE_CLEANUP_MIN_HALO_REDUCTION = 1.5;
|
|
41
|
+
const PREVIEW_EDGE_CLEANUP_STRONG_HALO_THRESHOLD = 4;
|
|
42
|
+
const PREVIEW_EDGE_CLEANUP_HALO_SPATIAL_THRESHOLD = 0.18;
|
|
43
|
+
const PREVIEW_EDGE_CLEANUP_PRESETS = Object.freeze([
|
|
44
|
+
{ minAlpha: 0.02, maxAlpha: 0.45, radius: 2, strength: 0.7, outsideAlphaMax: 0.05 },
|
|
45
|
+
{ minAlpha: 0.05, maxAlpha: 0.55, radius: 3, strength: 0.7, outsideAlphaMax: 0.08 },
|
|
46
|
+
{ minAlpha: 0.1, maxAlpha: 0.7, radius: 3, strength: 0.8, outsideAlphaMax: 0.12 },
|
|
47
|
+
{ minAlpha: 0.01, maxAlpha: 0.35, radius: 4, strength: 1.4, outsideAlphaMax: 0.05 }
|
|
48
|
+
]);
|
|
49
|
+
const PREVIEW_EDGE_CLEANUP_STRONG_GRADIENT_THRESHOLD = 0.45;
|
|
50
|
+
const PREVIEW_EDGE_CLEANUP_AGGRESSIVE_PRESETS = Object.freeze([
|
|
51
|
+
{
|
|
52
|
+
minAlpha: 0.01,
|
|
53
|
+
maxAlpha: 0.55,
|
|
54
|
+
radius: 2,
|
|
55
|
+
strength: 1.3,
|
|
56
|
+
outsideAlphaMax: 0.05,
|
|
57
|
+
minGradientImprovement: 0.12,
|
|
58
|
+
maxSpatialDrift: 0.18,
|
|
59
|
+
maxAcceptedSpatial: 0.18
|
|
60
|
+
}
|
|
61
|
+
]);
|
|
62
|
+
const FIRST_PASS_SIGN_FLIP_GRADIENT_THRESHOLD = 0.08;
|
|
63
|
+
const FIRST_PASS_SIGN_FLIP_MIN_GRADIENT_DROP = 0.2;
|
|
64
|
+
|
|
65
|
+
function nowMs() {
|
|
66
|
+
if (typeof globalThis.performance?.now === 'function') {
|
|
67
|
+
return globalThis.performance.now();
|
|
68
|
+
}
|
|
69
|
+
return Date.now();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cloneImageData(imageData) {
|
|
73
|
+
if (typeof ImageData !== 'undefined' && imageData instanceof ImageData) {
|
|
74
|
+
return new ImageData(
|
|
75
|
+
new Uint8ClampedArray(imageData.data),
|
|
76
|
+
imageData.width,
|
|
77
|
+
imageData.height
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
width: imageData.width,
|
|
83
|
+
height: imageData.height,
|
|
84
|
+
data: new Uint8ClampedArray(imageData.data)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeMetaPosition(position) {
|
|
89
|
+
if (!position) return null;
|
|
90
|
+
|
|
91
|
+
const { x, y, width, height } = position;
|
|
92
|
+
if (![x, y, width, height].every((value) => Number.isFinite(value))) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { x, y, width, height };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeMetaConfig(config) {
|
|
100
|
+
if (!config) return null;
|
|
101
|
+
|
|
102
|
+
const { logoSize, marginRight, marginBottom } = config;
|
|
103
|
+
if (![logoSize, marginRight, marginBottom].every((value) => Number.isFinite(value))) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { logoSize, marginRight, marginBottom };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createWatermarkMeta({
|
|
111
|
+
position = null,
|
|
112
|
+
config = null,
|
|
113
|
+
adaptiveConfidence = null,
|
|
114
|
+
originalSpatialScore = null,
|
|
115
|
+
originalGradientScore = null,
|
|
116
|
+
processedSpatialScore = null,
|
|
117
|
+
processedGradientScore = null,
|
|
118
|
+
suppressionGain = null,
|
|
119
|
+
templateWarp = null,
|
|
120
|
+
alphaGain = 1,
|
|
121
|
+
passCount = 0,
|
|
122
|
+
attemptedPassCount = 0,
|
|
123
|
+
passStopReason = null,
|
|
124
|
+
passes = null,
|
|
125
|
+
source = 'standard',
|
|
126
|
+
decisionTier = null,
|
|
127
|
+
applied = true,
|
|
128
|
+
skipReason = null,
|
|
129
|
+
subpixelShift = null,
|
|
130
|
+
selectionDebug = null
|
|
131
|
+
} = {}) {
|
|
132
|
+
const normalizedPosition = normalizeMetaPosition(position);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
applied,
|
|
136
|
+
skipReason: applied ? null : skipReason,
|
|
137
|
+
size: normalizedPosition ? normalizedPosition.width : null,
|
|
138
|
+
position: normalizedPosition,
|
|
139
|
+
config: normalizeMetaConfig(config),
|
|
140
|
+
detection: {
|
|
141
|
+
adaptiveConfidence,
|
|
142
|
+
originalSpatialScore,
|
|
143
|
+
originalGradientScore,
|
|
144
|
+
processedSpatialScore,
|
|
145
|
+
processedGradientScore,
|
|
146
|
+
suppressionGain
|
|
147
|
+
},
|
|
148
|
+
templateWarp: templateWarp ?? null,
|
|
149
|
+
alphaGain,
|
|
150
|
+
passCount,
|
|
151
|
+
attemptedPassCount,
|
|
152
|
+
passStopReason,
|
|
153
|
+
passes: Array.isArray(passes) ? passes : null,
|
|
154
|
+
// decisionTier is the normalized contract used by UI and attribution.
|
|
155
|
+
// source remains as a verbose execution trace for debugging/tests.
|
|
156
|
+
source,
|
|
157
|
+
decisionTier,
|
|
158
|
+
subpixelShift: subpixelShift ?? null,
|
|
159
|
+
selectionDebug
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function shouldRecalibrateAlphaStrength({ originalScore, processedScore, suppressionGain }) {
|
|
164
|
+
return originalScore >= 0.6 &&
|
|
165
|
+
processedScore >= RESIDUAL_RECALIBRATION_THRESHOLD &&
|
|
166
|
+
suppressionGain <= MIN_SUPPRESSION_FOR_SKIP_RECALIBRATION;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function shouldStopAfterFirstPass({
|
|
170
|
+
originalSpatialScore,
|
|
171
|
+
originalGradientScore,
|
|
172
|
+
firstPassSpatialScore,
|
|
173
|
+
firstPassGradientScore
|
|
174
|
+
}) {
|
|
175
|
+
if (Math.abs(firstPassSpatialScore) <= 0.25) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return originalSpatialScore >= 0 &&
|
|
180
|
+
firstPassSpatialScore < 0 &&
|
|
181
|
+
firstPassGradientScore <= FIRST_PASS_SIGN_FLIP_GRADIENT_THRESHOLD &&
|
|
182
|
+
(originalGradientScore - firstPassGradientScore) >= FIRST_PASS_SIGN_FLIP_MIN_GRADIENT_DROP;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function refineSubpixelOutline({
|
|
186
|
+
sourceImageData,
|
|
187
|
+
alphaMap,
|
|
188
|
+
position,
|
|
189
|
+
alphaGain,
|
|
190
|
+
originalNearBlackRatio,
|
|
191
|
+
baselineSpatialScore,
|
|
192
|
+
baselineGradientScore,
|
|
193
|
+
baselineShift,
|
|
194
|
+
minGain = OUTLINE_REFINEMENT_MIN_GAIN,
|
|
195
|
+
shiftCandidates = SUBPIXEL_REFINE_SHIFTS,
|
|
196
|
+
scaleCandidates = SUBPIXEL_REFINE_SCALES,
|
|
197
|
+
minGradientImprovement = 0.04,
|
|
198
|
+
maxSpatialDrift = 0.08
|
|
199
|
+
}) {
|
|
200
|
+
const size = position.width;
|
|
201
|
+
if (!size || size <= 8) return null;
|
|
202
|
+
if (alphaGain < minGain) return null;
|
|
203
|
+
|
|
204
|
+
const maxAllowedNearBlackRatio = Math.min(1, originalNearBlackRatio + MAX_NEAR_BLACK_RATIO_INCREASE);
|
|
205
|
+
const gainCandidates = [alphaGain];
|
|
206
|
+
const lower = Math.max(1, Number((alphaGain - 0.01).toFixed(2)));
|
|
207
|
+
const upper = Number((alphaGain + 0.01).toFixed(2));
|
|
208
|
+
if (lower !== alphaGain) gainCandidates.push(lower);
|
|
209
|
+
if (upper !== alphaGain) gainCandidates.push(upper);
|
|
210
|
+
|
|
211
|
+
const baseDx = baselineShift?.dx ?? 0;
|
|
212
|
+
const baseDy = baselineShift?.dy ?? 0;
|
|
213
|
+
const baseScale = baselineShift?.scale ?? 1;
|
|
214
|
+
|
|
215
|
+
let best = null;
|
|
216
|
+
for (const scaleDelta of scaleCandidates) {
|
|
217
|
+
const scale = Number((baseScale * scaleDelta).toFixed(4));
|
|
218
|
+
for (const dyDelta of shiftCandidates) {
|
|
219
|
+
const dy = baseDy + dyDelta;
|
|
220
|
+
for (const dxDelta of shiftCandidates) {
|
|
221
|
+
const dx = baseDx + dxDelta;
|
|
222
|
+
const warped = warpAlphaMap(alphaMap, size, { dx, dy, scale });
|
|
223
|
+
for (const gain of gainCandidates) {
|
|
224
|
+
const candidate = cloneImageData(sourceImageData);
|
|
225
|
+
removeWatermark(candidate, warped, position, { alphaGain: gain });
|
|
226
|
+
const nearBlackRatio = calculateNearBlackRatio(candidate, position);
|
|
227
|
+
if (nearBlackRatio > maxAllowedNearBlackRatio) continue;
|
|
228
|
+
|
|
229
|
+
const spatialScore = computeRegionSpatialCorrelation({
|
|
230
|
+
imageData: candidate,
|
|
231
|
+
alphaMap: warped,
|
|
232
|
+
region: { x: position.x, y: position.y, size }
|
|
233
|
+
});
|
|
234
|
+
const gradientScore = computeRegionGradientCorrelation({
|
|
235
|
+
imageData: candidate,
|
|
236
|
+
alphaMap: warped,
|
|
237
|
+
region: { x: position.x, y: position.y, size }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const cost = Math.abs(spatialScore) * 0.6 + Math.max(0, gradientScore);
|
|
241
|
+
if (!best || cost < best.cost) {
|
|
242
|
+
best = {
|
|
243
|
+
imageData: candidate,
|
|
244
|
+
alphaMap: warped,
|
|
245
|
+
alphaGain: gain,
|
|
246
|
+
shift: { dx, dy, scale },
|
|
247
|
+
spatialScore,
|
|
248
|
+
gradientScore,
|
|
249
|
+
nearBlackRatio,
|
|
250
|
+
cost
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!best) return null;
|
|
259
|
+
|
|
260
|
+
const improvedGradient = best.gradientScore <= baselineGradientScore - minGradientImprovement;
|
|
261
|
+
const keptSpatial = Math.abs(best.spatialScore) <= Math.abs(baselineSpatialScore) + maxSpatialDrift;
|
|
262
|
+
if (!improvedGradient || !keptSpatial) return null;
|
|
263
|
+
|
|
264
|
+
return best;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function recalibrateAlphaStrength({
|
|
268
|
+
sourceImageData,
|
|
269
|
+
alphaMap,
|
|
270
|
+
position,
|
|
271
|
+
originalSpatialScore,
|
|
272
|
+
processedSpatialScore,
|
|
273
|
+
originalNearBlackRatio
|
|
274
|
+
}) {
|
|
275
|
+
let bestScore = processedSpatialScore;
|
|
276
|
+
let bestGain = 1;
|
|
277
|
+
let bestImageData = null;
|
|
278
|
+
const maxAllowedNearBlackRatio = Math.min(1, originalNearBlackRatio + MAX_NEAR_BLACK_RATIO_INCREASE);
|
|
279
|
+
|
|
280
|
+
for (const alphaGain of ALPHA_GAIN_CANDIDATES) {
|
|
281
|
+
const candidate = cloneImageData(sourceImageData);
|
|
282
|
+
removeWatermark(candidate, alphaMap, position, { alphaGain });
|
|
283
|
+
const candidateNearBlackRatio = calculateNearBlackRatio(candidate, position);
|
|
284
|
+
if (candidateNearBlackRatio > maxAllowedNearBlackRatio) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const score = computeRegionSpatialCorrelation({
|
|
289
|
+
imageData: candidate,
|
|
290
|
+
alphaMap,
|
|
291
|
+
region: {
|
|
292
|
+
x: position.x,
|
|
293
|
+
y: position.y,
|
|
294
|
+
size: position.width
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
if (score < bestScore) {
|
|
299
|
+
bestScore = score;
|
|
300
|
+
bestGain = alphaGain;
|
|
301
|
+
bestImageData = candidate;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const refinedCandidates = [];
|
|
306
|
+
for (let delta = -0.05; delta <= 0.05; delta += 0.01) {
|
|
307
|
+
refinedCandidates.push(Number((bestGain + delta).toFixed(2)));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const alphaGain of refinedCandidates) {
|
|
311
|
+
if (alphaGain <= 1 || alphaGain >= 3) continue;
|
|
312
|
+
const candidate = cloneImageData(sourceImageData);
|
|
313
|
+
removeWatermark(candidate, alphaMap, position, { alphaGain });
|
|
314
|
+
const candidateNearBlackRatio = calculateNearBlackRatio(candidate, position);
|
|
315
|
+
if (candidateNearBlackRatio > maxAllowedNearBlackRatio) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const score = computeRegionSpatialCorrelation({
|
|
320
|
+
imageData: candidate,
|
|
321
|
+
alphaMap,
|
|
322
|
+
region: {
|
|
323
|
+
x: position.x,
|
|
324
|
+
y: position.y,
|
|
325
|
+
size: position.width
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (score < bestScore) {
|
|
330
|
+
bestScore = score;
|
|
331
|
+
bestGain = alphaGain;
|
|
332
|
+
bestImageData = candidate;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const scoreDelta = processedSpatialScore - bestScore;
|
|
337
|
+
if (!bestImageData || scoreDelta < MIN_RECALIBRATION_SCORE_DELTA) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
imageData: bestImageData,
|
|
343
|
+
alphaGain: bestGain,
|
|
344
|
+
processedSpatialScore: bestScore,
|
|
345
|
+
suppressionGain: originalSpatialScore - bestScore
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function shouldRefinePreviewResidualEdge({
|
|
350
|
+
source,
|
|
351
|
+
position,
|
|
352
|
+
baselineSpatialScore,
|
|
353
|
+
baselineGradientScore,
|
|
354
|
+
baselinePositiveHalo
|
|
355
|
+
}) {
|
|
356
|
+
return typeof source === 'string' &&
|
|
357
|
+
source.includes('preview-anchor') &&
|
|
358
|
+
position?.width >= 24 &&
|
|
359
|
+
position?.width <= PREVIEW_EDGE_CLEANUP_MAX_SIZE &&
|
|
360
|
+
(
|
|
361
|
+
Math.abs(baselineSpatialScore) <= PREVIEW_EDGE_CLEANUP_SPATIAL_THRESHOLD ||
|
|
362
|
+
(
|
|
363
|
+
baselinePositiveHalo >= PREVIEW_EDGE_CLEANUP_STRONG_HALO_THRESHOLD &&
|
|
364
|
+
Math.abs(baselineSpatialScore) <= PREVIEW_EDGE_CLEANUP_HALO_SPATIAL_THRESHOLD
|
|
365
|
+
)
|
|
366
|
+
) &&
|
|
367
|
+
baselineGradientScore >= PREVIEW_EDGE_CLEANUP_GRADIENT_THRESHOLD;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function shouldUsePreviewAnchorFastCleanup(selectedTrial, position) {
|
|
371
|
+
return selectedTrial?.provenance?.previewAnchor === true &&
|
|
372
|
+
position?.width >= 24 &&
|
|
373
|
+
position?.width <= PREVIEW_EDGE_CLEANUP_MAX_SIZE;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function blendPreviewResidualEdge({
|
|
377
|
+
sourceImageData,
|
|
378
|
+
alphaMap,
|
|
379
|
+
position,
|
|
380
|
+
minAlpha,
|
|
381
|
+
maxAlpha,
|
|
382
|
+
radius,
|
|
383
|
+
strength,
|
|
384
|
+
outsideAlphaMax
|
|
385
|
+
}) {
|
|
386
|
+
const candidate = cloneImageData(sourceImageData);
|
|
387
|
+
const { width: imageWidth, height: imageHeight, data } = sourceImageData;
|
|
388
|
+
const regionSize = position.width;
|
|
389
|
+
const maxAlphaSafe = Math.max(maxAlpha, 1e-6);
|
|
390
|
+
|
|
391
|
+
for (let row = 0; row < regionSize; row++) {
|
|
392
|
+
for (let col = 0; col < regionSize; col++) {
|
|
393
|
+
const alpha = alphaMap[row * regionSize + col];
|
|
394
|
+
if (alpha < minAlpha || alpha > maxAlpha) continue;
|
|
395
|
+
|
|
396
|
+
let sumR = 0;
|
|
397
|
+
let sumG = 0;
|
|
398
|
+
let sumB = 0;
|
|
399
|
+
let sumWeight = 0;
|
|
400
|
+
|
|
401
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
402
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
403
|
+
if (dx === 0 && dy === 0) continue;
|
|
404
|
+
|
|
405
|
+
const localY = row + dy;
|
|
406
|
+
const localX = col + dx;
|
|
407
|
+
const pixelX = position.x + localX;
|
|
408
|
+
const pixelY = position.y + localY;
|
|
409
|
+
|
|
410
|
+
if (pixelX < 0 || pixelY < 0 || pixelX >= imageWidth || pixelY >= imageHeight) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let neighborAlpha = 0;
|
|
415
|
+
if (localY >= 0 && localX >= 0 && localY < regionSize && localX < regionSize) {
|
|
416
|
+
neighborAlpha = alphaMap[localY * regionSize + localX];
|
|
417
|
+
}
|
|
418
|
+
if (neighborAlpha > outsideAlphaMax) continue;
|
|
419
|
+
|
|
420
|
+
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
421
|
+
const weight = 1 / distance;
|
|
422
|
+
const pixelIndex = (pixelY * imageWidth + pixelX) * 4;
|
|
423
|
+
sumR += data[pixelIndex] * weight;
|
|
424
|
+
sumG += data[pixelIndex + 1] * weight;
|
|
425
|
+
sumB += data[pixelIndex + 2] * weight;
|
|
426
|
+
sumWeight += weight;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (sumWeight <= 0) continue;
|
|
431
|
+
|
|
432
|
+
const blend = Math.max(0, Math.min(1, strength * alpha / maxAlphaSafe));
|
|
433
|
+
const pixelIndex = ((position.y + row) * imageWidth + (position.x + col)) * 4;
|
|
434
|
+
candidate.data[pixelIndex] = Math.round(data[pixelIndex] * (1 - blend) + (sumR / sumWeight) * blend);
|
|
435
|
+
candidate.data[pixelIndex + 1] = Math.round(data[pixelIndex + 1] * (1 - blend) + (sumG / sumWeight) * blend);
|
|
436
|
+
candidate.data[pixelIndex + 2] = Math.round(data[pixelIndex + 2] * (1 - blend) + (sumB / sumWeight) * blend);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return candidate;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function refinePreviewResidualEdge({
|
|
444
|
+
sourceImageData,
|
|
445
|
+
alphaMap,
|
|
446
|
+
position,
|
|
447
|
+
source,
|
|
448
|
+
baselineSpatialScore,
|
|
449
|
+
baselineGradientScore,
|
|
450
|
+
minGradientImprovement = PREVIEW_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT,
|
|
451
|
+
maxSpatialDrift = PREVIEW_EDGE_CLEANUP_MAX_SPATIAL_DRIFT,
|
|
452
|
+
allowAggressivePresets = false
|
|
453
|
+
}) {
|
|
454
|
+
const baselineHalo = assessAlphaBandHalo({
|
|
455
|
+
imageData: sourceImageData,
|
|
456
|
+
position,
|
|
457
|
+
alphaMap
|
|
458
|
+
});
|
|
459
|
+
const baselinePositiveHalo = baselineHalo.positiveDeltaLum;
|
|
460
|
+
if (!shouldRefinePreviewResidualEdge({
|
|
461
|
+
source,
|
|
462
|
+
position,
|
|
463
|
+
baselineSpatialScore,
|
|
464
|
+
baselineGradientScore,
|
|
465
|
+
baselinePositiveHalo
|
|
466
|
+
})) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const baselineNearBlackRatio = calculateNearBlackRatio(sourceImageData, position);
|
|
471
|
+
const maxAllowedNearBlackRatio = Math.min(1, baselineNearBlackRatio + MAX_NEAR_BLACK_RATIO_INCREASE);
|
|
472
|
+
const resolvedMinGradientImprovement = baselineGradientScore <= PREVIEW_EDGE_CLEANUP_FINE_GRADIENT_THRESHOLD
|
|
473
|
+
? PREVIEW_EDGE_CLEANUP_FINE_MIN_GRADIENT_IMPROVEMENT
|
|
474
|
+
: (
|
|
475
|
+
baselinePositiveHalo >= PREVIEW_EDGE_CLEANUP_STRONG_HALO_THRESHOLD
|
|
476
|
+
? PREVIEW_EDGE_CLEANUP_HALO_RELAXED_MIN_GRADIENT_IMPROVEMENT
|
|
477
|
+
: minGradientImprovement
|
|
478
|
+
);
|
|
479
|
+
const presets = allowAggressivePresets &&
|
|
480
|
+
baselineGradientScore >= PREVIEW_EDGE_CLEANUP_STRONG_GRADIENT_THRESHOLD &&
|
|
481
|
+
Math.abs(baselineSpatialScore) <= 0.05
|
|
482
|
+
? [...PREVIEW_EDGE_CLEANUP_PRESETS, ...PREVIEW_EDGE_CLEANUP_AGGRESSIVE_PRESETS]
|
|
483
|
+
: PREVIEW_EDGE_CLEANUP_PRESETS;
|
|
484
|
+
let best = null;
|
|
485
|
+
|
|
486
|
+
for (const preset of presets) {
|
|
487
|
+
const candidate = blendPreviewResidualEdge({
|
|
488
|
+
sourceImageData,
|
|
489
|
+
alphaMap,
|
|
490
|
+
position,
|
|
491
|
+
...preset
|
|
492
|
+
});
|
|
493
|
+
const nearBlackRatio = calculateNearBlackRatio(candidate, position);
|
|
494
|
+
if (nearBlackRatio > maxAllowedNearBlackRatio) continue;
|
|
495
|
+
|
|
496
|
+
const spatialScore = computeRegionSpatialCorrelation({
|
|
497
|
+
imageData: candidate,
|
|
498
|
+
alphaMap,
|
|
499
|
+
region: { x: position.x, y: position.y, size: position.width }
|
|
500
|
+
});
|
|
501
|
+
const gradientScore = computeRegionGradientCorrelation({
|
|
502
|
+
imageData: candidate,
|
|
503
|
+
alphaMap,
|
|
504
|
+
region: { x: position.x, y: position.y, size: position.width }
|
|
505
|
+
});
|
|
506
|
+
const halo = assessAlphaBandHalo({
|
|
507
|
+
imageData: candidate,
|
|
508
|
+
position,
|
|
509
|
+
alphaMap
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const presetMinGradientImprovement = preset.minGradientImprovement ?? resolvedMinGradientImprovement;
|
|
513
|
+
const presetMaxSpatialDrift = preset.maxSpatialDrift ?? maxSpatialDrift;
|
|
514
|
+
const presetMaxAcceptedSpatial = preset.maxAcceptedSpatial ?? 0.22;
|
|
515
|
+
const improvedGradient = gradientScore <= baselineGradientScore - presetMinGradientImprovement;
|
|
516
|
+
const keptSpatial = Math.abs(spatialScore) <= Math.abs(baselineSpatialScore) + presetMaxSpatialDrift;
|
|
517
|
+
const keptResidualWithinTarget = Math.abs(spatialScore) <= presetMaxAcceptedSpatial;
|
|
518
|
+
const candidatePositiveHalo = halo.positiveDeltaLum;
|
|
519
|
+
const improvedHalo = baselinePositiveHalo < PREVIEW_EDGE_CLEANUP_STRONG_HALO_THRESHOLD ||
|
|
520
|
+
candidatePositiveHalo <= baselinePositiveHalo - PREVIEW_EDGE_CLEANUP_MIN_HALO_REDUCTION;
|
|
521
|
+
if (!improvedGradient || !keptSpatial || !keptResidualWithinTarget || !improvedHalo) continue;
|
|
522
|
+
|
|
523
|
+
const cost = Math.abs(spatialScore) * 0.6 +
|
|
524
|
+
Math.max(0, gradientScore) +
|
|
525
|
+
candidatePositiveHalo * PREVIEW_EDGE_CLEANUP_HALO_WEIGHT;
|
|
526
|
+
if (!best || cost < best.cost) {
|
|
527
|
+
best = {
|
|
528
|
+
imageData: candidate,
|
|
529
|
+
spatialScore,
|
|
530
|
+
gradientScore,
|
|
531
|
+
halo,
|
|
532
|
+
cost
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return best;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function processWatermarkImageData(imageData, options = {}) {
|
|
541
|
+
const totalStartedAt = nowMs();
|
|
542
|
+
const debugTimingsEnabled = options.debugTimings === true;
|
|
543
|
+
const debugTimings = debugTimingsEnabled ? {} : null;
|
|
544
|
+
const adaptiveMode = options.adaptiveMode || 'auto';
|
|
545
|
+
const allowAdaptiveSearch =
|
|
546
|
+
adaptiveMode !== 'never' &&
|
|
547
|
+
adaptiveMode !== 'off';
|
|
548
|
+
const originalImageData = cloneImageData(imageData);
|
|
549
|
+
const { alpha48, alpha96 } = options;
|
|
550
|
+
const alphaGainCandidates = ALPHA_GAIN_CANDIDATES;
|
|
551
|
+
|
|
552
|
+
if (!alpha48 || !alpha96) {
|
|
553
|
+
throw new Error('processWatermarkImageData requires alpha48 and alpha96');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const defaultConfig = detectWatermarkConfig(originalImageData.width, originalImageData.height);
|
|
557
|
+
const resolvedConfig = resolveInitialStandardConfig({
|
|
558
|
+
imageData: originalImageData,
|
|
559
|
+
defaultConfig,
|
|
560
|
+
alpha48,
|
|
561
|
+
alpha96
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
let config = resolvedConfig;
|
|
565
|
+
let position = calculateWatermarkPosition(originalImageData.width, originalImageData.height, config);
|
|
566
|
+
let alphaMap = config.logoSize === 96 ? alpha96 : alpha48;
|
|
567
|
+
let source = 'standard';
|
|
568
|
+
let adaptiveConfidence = null;
|
|
569
|
+
let alphaGain = 1;
|
|
570
|
+
let subpixelShift = null;
|
|
571
|
+
let templateWarp = null;
|
|
572
|
+
let decisionTier = null;
|
|
573
|
+
let passCount = 0;
|
|
574
|
+
let attemptedPassCount = 0;
|
|
575
|
+
let passStopReason = null;
|
|
576
|
+
let passes = null;
|
|
577
|
+
|
|
578
|
+
const initialSelectionStartedAt = nowMs();
|
|
579
|
+
const initialSelection = selectInitialCandidate({
|
|
580
|
+
originalImageData,
|
|
581
|
+
config,
|
|
582
|
+
position,
|
|
583
|
+
alpha48,
|
|
584
|
+
alpha96,
|
|
585
|
+
getAlphaMap: options.getAlphaMap,
|
|
586
|
+
allowAdaptiveSearch,
|
|
587
|
+
alphaGainCandidates
|
|
588
|
+
});
|
|
589
|
+
if (debugTimingsEnabled) {
|
|
590
|
+
debugTimings.initialSelectionMs = nowMs() - initialSelectionStartedAt;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!initialSelection.selectedTrial) {
|
|
594
|
+
if (debugTimingsEnabled) {
|
|
595
|
+
debugTimings.totalMs = nowMs() - totalStartedAt;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
imageData: originalImageData,
|
|
599
|
+
meta: createWatermarkMeta({
|
|
600
|
+
adaptiveConfidence: initialSelection.adaptiveConfidence,
|
|
601
|
+
originalSpatialScore: initialSelection.standardSpatialScore,
|
|
602
|
+
originalGradientScore: initialSelection.standardGradientScore,
|
|
603
|
+
processedSpatialScore: initialSelection.standardSpatialScore,
|
|
604
|
+
processedGradientScore: initialSelection.standardGradientScore,
|
|
605
|
+
suppressionGain: 0,
|
|
606
|
+
alphaGain: 1,
|
|
607
|
+
source: 'skipped',
|
|
608
|
+
decisionTier: initialSelection.decisionTier ?? 'insufficient',
|
|
609
|
+
applied: false,
|
|
610
|
+
skipReason: 'no-watermark-detected',
|
|
611
|
+
selectionDebug: null
|
|
612
|
+
}),
|
|
613
|
+
debugTimings
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
position = initialSelection.position;
|
|
618
|
+
alphaMap = initialSelection.alphaMap;
|
|
619
|
+
config = initialSelection.config;
|
|
620
|
+
source = initialSelection.source;
|
|
621
|
+
adaptiveConfidence = initialSelection.adaptiveConfidence;
|
|
622
|
+
templateWarp = initialSelection.templateWarp;
|
|
623
|
+
alphaGain = initialSelection.alphaGain;
|
|
624
|
+
decisionTier = initialSelection.decisionTier;
|
|
625
|
+
|
|
626
|
+
const selectedTrial = initialSelection.selectedTrial;
|
|
627
|
+
const usePreviewAnchorFastCleanup = shouldUsePreviewAnchorFastCleanup(selectedTrial, position);
|
|
628
|
+
const skipPreviewAnchorMultiPass = selectedTrial?.provenance?.previewAnchor === true;
|
|
629
|
+
|
|
630
|
+
let finalImageData = selectedTrial.imageData;
|
|
631
|
+
|
|
632
|
+
let originalSpatialScore = selectedTrial.originalSpatialScore;
|
|
633
|
+
let originalGradientScore = selectedTrial.originalGradientScore;
|
|
634
|
+
|
|
635
|
+
const firstPassMetricsStartedAt = nowMs();
|
|
636
|
+
const firstPassSpatialScore = computeRegionSpatialCorrelation({
|
|
637
|
+
imageData: finalImageData,
|
|
638
|
+
alphaMap,
|
|
639
|
+
region: { x: position.x, y: position.y, size: position.width }
|
|
640
|
+
});
|
|
641
|
+
const firstPassGradientScore = computeRegionGradientCorrelation({
|
|
642
|
+
imageData: finalImageData,
|
|
643
|
+
alphaMap,
|
|
644
|
+
region: { x: position.x, y: position.y, size: position.width }
|
|
645
|
+
});
|
|
646
|
+
const firstPassNearBlackRatio = calculateNearBlackRatio(finalImageData, position);
|
|
647
|
+
const firstPassRecord = {
|
|
648
|
+
index: 1,
|
|
649
|
+
beforeSpatialScore: originalSpatialScore,
|
|
650
|
+
beforeGradientScore: originalGradientScore,
|
|
651
|
+
afterSpatialScore: firstPassSpatialScore,
|
|
652
|
+
afterGradientScore: firstPassGradientScore,
|
|
653
|
+
improvement: Math.abs(originalSpatialScore) - Math.abs(firstPassSpatialScore),
|
|
654
|
+
gradientDelta: firstPassGradientScore - originalGradientScore,
|
|
655
|
+
nearBlackRatio: firstPassNearBlackRatio
|
|
656
|
+
};
|
|
657
|
+
if (debugTimingsEnabled) {
|
|
658
|
+
debugTimings.firstPassMetricsMs = nowMs() - firstPassMetricsStartedAt;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const totalMaxPasses = Math.max(
|
|
662
|
+
1,
|
|
663
|
+
options.maxPasses ?? 4
|
|
664
|
+
);
|
|
665
|
+
const remainingPasses = Math.max(0, totalMaxPasses - 1);
|
|
666
|
+
const firstPassClearedResidual = shouldStopAfterFirstPass({
|
|
667
|
+
originalSpatialScore,
|
|
668
|
+
originalGradientScore,
|
|
669
|
+
firstPassSpatialScore,
|
|
670
|
+
firstPassGradientScore
|
|
671
|
+
});
|
|
672
|
+
const extraPassStartedAt = nowMs();
|
|
673
|
+
const extraPassResult = remainingPasses > 0 &&
|
|
674
|
+
!firstPassClearedResidual &&
|
|
675
|
+
!skipPreviewAnchorMultiPass
|
|
676
|
+
? removeRepeatedWatermarkLayers({
|
|
677
|
+
imageData: finalImageData,
|
|
678
|
+
alphaMap,
|
|
679
|
+
position,
|
|
680
|
+
maxPasses: remainingPasses,
|
|
681
|
+
startingPassIndex: 1,
|
|
682
|
+
alphaGain
|
|
683
|
+
})
|
|
684
|
+
: null;
|
|
685
|
+
if (debugTimingsEnabled) {
|
|
686
|
+
debugTimings.extraPassMs = nowMs() - extraPassStartedAt;
|
|
687
|
+
}
|
|
688
|
+
finalImageData = extraPassResult?.imageData ?? finalImageData;
|
|
689
|
+
passCount = extraPassResult?.passCount ?? 1;
|
|
690
|
+
attemptedPassCount = extraPassResult?.attemptedPassCount ?? 1;
|
|
691
|
+
passStopReason = extraPassResult?.stopReason ?? (
|
|
692
|
+
firstPassClearedResidual
|
|
693
|
+
? 'residual-low'
|
|
694
|
+
: (skipPreviewAnchorMultiPass ? 'preview-anchor-single-pass' : 'max-passes')
|
|
695
|
+
);
|
|
696
|
+
passes = [firstPassRecord, ...(extraPassResult?.passes ?? [])];
|
|
697
|
+
if (passCount > 1) {
|
|
698
|
+
source = `${source}+multipass`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const finalMetricsStartedAt = nowMs();
|
|
702
|
+
const processedSpatialScore = computeRegionSpatialCorrelation({
|
|
703
|
+
imageData: finalImageData,
|
|
704
|
+
alphaMap,
|
|
705
|
+
region: {
|
|
706
|
+
x: position.x,
|
|
707
|
+
y: position.y,
|
|
708
|
+
size: position.width
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
const processedGradientScore = computeRegionGradientCorrelation({
|
|
712
|
+
imageData: finalImageData,
|
|
713
|
+
alphaMap,
|
|
714
|
+
region: {
|
|
715
|
+
x: position.x,
|
|
716
|
+
y: position.y,
|
|
717
|
+
size: position.width
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
if (debugTimingsEnabled) {
|
|
721
|
+
debugTimings.finalMetricsMs = nowMs() - finalMetricsStartedAt;
|
|
722
|
+
}
|
|
723
|
+
let finalProcessedSpatialScore = processedSpatialScore;
|
|
724
|
+
let finalProcessedGradientScore = processedGradientScore;
|
|
725
|
+
let suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
|
|
726
|
+
|
|
727
|
+
const recalibrationStartedAt = nowMs();
|
|
728
|
+
if (shouldRecalibrateAlphaStrength({
|
|
729
|
+
originalScore: originalSpatialScore,
|
|
730
|
+
processedScore: finalProcessedSpatialScore,
|
|
731
|
+
suppressionGain
|
|
732
|
+
})) {
|
|
733
|
+
const originalNearBlackRatio = calculateNearBlackRatio(finalImageData, position);
|
|
734
|
+
const recalibrated = recalibrateAlphaStrength({
|
|
735
|
+
sourceImageData: finalImageData,
|
|
736
|
+
alphaMap,
|
|
737
|
+
position,
|
|
738
|
+
originalSpatialScore,
|
|
739
|
+
processedSpatialScore: finalProcessedSpatialScore,
|
|
740
|
+
originalNearBlackRatio
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (recalibrated) {
|
|
744
|
+
finalImageData = recalibrated.imageData;
|
|
745
|
+
alphaGain = recalibrated.alphaGain;
|
|
746
|
+
finalProcessedSpatialScore = recalibrated.processedSpatialScore;
|
|
747
|
+
finalProcessedGradientScore = computeRegionGradientCorrelation({
|
|
748
|
+
imageData: finalImageData,
|
|
749
|
+
alphaMap,
|
|
750
|
+
region: {
|
|
751
|
+
x: position.x,
|
|
752
|
+
y: position.y,
|
|
753
|
+
size: position.width
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
suppressionGain = recalibrated.suppressionGain;
|
|
757
|
+
source = source === 'adaptive' ? 'adaptive+gain' : `${source}+gain`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (debugTimingsEnabled) {
|
|
761
|
+
debugTimings.recalibrationMs = nowMs() - recalibrationStartedAt;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
let previewEdgeCleanupElapsedMs = 0;
|
|
765
|
+
const applyPreviewEdgeCleanup = () => {
|
|
766
|
+
const previewEdgeStartedAt = nowMs();
|
|
767
|
+
const previewEdgeRefined = refinePreviewResidualEdge({
|
|
768
|
+
sourceImageData: finalImageData,
|
|
769
|
+
alphaMap,
|
|
770
|
+
position,
|
|
771
|
+
source,
|
|
772
|
+
baselineSpatialScore: finalProcessedSpatialScore,
|
|
773
|
+
baselineGradientScore: finalProcessedGradientScore,
|
|
774
|
+
allowAggressivePresets: usePreviewAnchorFastCleanup
|
|
775
|
+
});
|
|
776
|
+
previewEdgeCleanupElapsedMs += nowMs() - previewEdgeStartedAt;
|
|
777
|
+
|
|
778
|
+
if (!previewEdgeRefined) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
finalImageData = previewEdgeRefined.imageData;
|
|
783
|
+
finalProcessedSpatialScore = previewEdgeRefined.spatialScore;
|
|
784
|
+
finalProcessedGradientScore = previewEdgeRefined.gradientScore;
|
|
785
|
+
suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
|
|
786
|
+
source = `${source}+edge-cleanup`;
|
|
787
|
+
return true;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const subpixelStartedAt = nowMs();
|
|
791
|
+
if (
|
|
792
|
+
!usePreviewAnchorFastCleanup &&
|
|
793
|
+
finalProcessedSpatialScore <= 0.3 &&
|
|
794
|
+
finalProcessedGradientScore >= OUTLINE_REFINEMENT_THRESHOLD
|
|
795
|
+
) {
|
|
796
|
+
const originalNearBlackRatio = calculateNearBlackRatio(finalImageData, position);
|
|
797
|
+
const baselineShift = templateWarp ?? { dx: 0, dy: 0, scale: 1 };
|
|
798
|
+
const refined = refineSubpixelOutline({
|
|
799
|
+
sourceImageData: finalImageData,
|
|
800
|
+
alphaMap,
|
|
801
|
+
position,
|
|
802
|
+
alphaGain,
|
|
803
|
+
originalNearBlackRatio,
|
|
804
|
+
baselineSpatialScore: finalProcessedSpatialScore,
|
|
805
|
+
baselineGradientScore: finalProcessedGradientScore,
|
|
806
|
+
baselineShift,
|
|
807
|
+
minGain: OUTLINE_REFINEMENT_MIN_GAIN,
|
|
808
|
+
shiftCandidates: SUBPIXEL_REFINE_SHIFTS,
|
|
809
|
+
scaleCandidates: SUBPIXEL_REFINE_SCALES,
|
|
810
|
+
minGradientImprovement: 0.04,
|
|
811
|
+
maxSpatialDrift: 0.08
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (refined) {
|
|
815
|
+
finalImageData = refined.imageData;
|
|
816
|
+
alphaMap = refined.alphaMap;
|
|
817
|
+
alphaGain = refined.alphaGain;
|
|
818
|
+
finalProcessedSpatialScore = refined.spatialScore;
|
|
819
|
+
finalProcessedGradientScore = refined.gradientScore;
|
|
820
|
+
suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
|
|
821
|
+
source = `${source}+subpixel`;
|
|
822
|
+
subpixelShift = refined.shift;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (debugTimingsEnabled) {
|
|
826
|
+
debugTimings.subpixelRefinementMs = nowMs() - subpixelStartedAt;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let previewEdgeCleanupPassCount = 0;
|
|
830
|
+
while (previewEdgeCleanupPassCount < PREVIEW_EDGE_CLEANUP_MAX_APPLIED_PASSES) {
|
|
831
|
+
if (!applyPreviewEdgeCleanup()) {
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
previewEdgeCleanupPassCount++;
|
|
835
|
+
}
|
|
836
|
+
if (debugTimingsEnabled) {
|
|
837
|
+
debugTimings.previewEdgeCleanupMs = previewEdgeCleanupElapsedMs;
|
|
838
|
+
debugTimings.totalMs = nowMs() - totalStartedAt;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return {
|
|
842
|
+
imageData: finalImageData,
|
|
843
|
+
meta: createWatermarkMeta({
|
|
844
|
+
position,
|
|
845
|
+
config,
|
|
846
|
+
adaptiveConfidence,
|
|
847
|
+
originalSpatialScore,
|
|
848
|
+
originalGradientScore,
|
|
849
|
+
processedSpatialScore: finalProcessedSpatialScore,
|
|
850
|
+
processedGradientScore: finalProcessedGradientScore,
|
|
851
|
+
suppressionGain,
|
|
852
|
+
templateWarp,
|
|
853
|
+
alphaGain,
|
|
854
|
+
passCount,
|
|
855
|
+
attemptedPassCount,
|
|
856
|
+
passStopReason,
|
|
857
|
+
passes,
|
|
858
|
+
source,
|
|
859
|
+
decisionTier,
|
|
860
|
+
applied: true,
|
|
861
|
+
subpixelShift,
|
|
862
|
+
selectionDebug: createSelectionDebugSummary({
|
|
863
|
+
selectedTrial,
|
|
864
|
+
selectionSource: initialSelection.source,
|
|
865
|
+
initialConfig: resolvedConfig,
|
|
866
|
+
initialPosition: calculateWatermarkPosition(
|
|
867
|
+
originalImageData.width,
|
|
868
|
+
originalImageData.height,
|
|
869
|
+
resolvedConfig
|
|
870
|
+
)
|
|
871
|
+
})
|
|
872
|
+
}),
|
|
873
|
+
debugTimings
|
|
874
|
+
};
|
|
875
|
+
}
|