@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,822 @@
|
|
|
1
|
+
import { warpAlphaMap } from './adaptiveDetector.js';
|
|
2
|
+
import { removeWatermark } from './blendModes.js';
|
|
3
|
+
|
|
4
|
+
function clamp01(value) {
|
|
5
|
+
if (!Number.isFinite(value)) return 0;
|
|
6
|
+
if (value <= 0) return 0;
|
|
7
|
+
if (value >= 1) return 1;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveChannelAlpha(original, preview) {
|
|
12
|
+
const denominator = 255 - original;
|
|
13
|
+
if (!Number.isFinite(denominator) || denominator <= 0) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return clamp01((preview - original) / denominator);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function estimatePreviewAlphaMap({
|
|
21
|
+
sourceImageData,
|
|
22
|
+
previewImageData,
|
|
23
|
+
position
|
|
24
|
+
}) {
|
|
25
|
+
if (!sourceImageData || !previewImageData || !position) {
|
|
26
|
+
throw new TypeError('estimatePreviewAlphaMap requires sourceImageData, previewImageData, and position');
|
|
27
|
+
}
|
|
28
|
+
if (sourceImageData.width !== previewImageData.width || sourceImageData.height !== previewImageData.height) {
|
|
29
|
+
throw new RangeError('sourceImageData and previewImageData must have identical dimensions');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { x, y, width, height } = position;
|
|
33
|
+
if (![x, y, width, height].every((value) => Number.isInteger(value) && value >= 0)) {
|
|
34
|
+
throw new RangeError('position must contain non-negative integer bounds');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const alphaMap = new Float32Array(width * height);
|
|
38
|
+
for (let row = 0; row < height; row++) {
|
|
39
|
+
for (let col = 0; col < width; col++) {
|
|
40
|
+
const idx = ((y + row) * sourceImageData.width + (x + col)) * 4;
|
|
41
|
+
const r = resolveChannelAlpha(sourceImageData.data[idx], previewImageData.data[idx]);
|
|
42
|
+
const g = resolveChannelAlpha(sourceImageData.data[idx + 1], previewImageData.data[idx + 1]);
|
|
43
|
+
const b = resolveChannelAlpha(sourceImageData.data[idx + 2], previewImageData.data[idx + 2]);
|
|
44
|
+
|
|
45
|
+
alphaMap[row * width + col] = clamp01(Math.max(r, g, b));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return alphaMap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function aggregatePreviewAlphaMaps(alphaMaps) {
|
|
53
|
+
if (!Array.isArray(alphaMaps) || alphaMaps.length === 0) {
|
|
54
|
+
throw new TypeError('aggregatePreviewAlphaMaps requires at least one alpha map');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const expectedLength = alphaMaps[0]?.length;
|
|
58
|
+
if (!Number.isInteger(expectedLength) || expectedLength <= 0) {
|
|
59
|
+
throw new TypeError('alpha maps must be typed arrays with a positive length');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const alphaMap of alphaMaps) {
|
|
63
|
+
if (!alphaMap || alphaMap.length !== expectedLength) {
|
|
64
|
+
throw new RangeError('all alpha maps must have identical lengths');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const aggregated = new Float32Array(expectedLength);
|
|
69
|
+
for (let i = 0; i < expectedLength; i++) {
|
|
70
|
+
const values = alphaMaps
|
|
71
|
+
.map((alphaMap) => clamp01(alphaMap[i]))
|
|
72
|
+
.sort((left, right) => left - right);
|
|
73
|
+
const middle = Math.floor(values.length / 2);
|
|
74
|
+
|
|
75
|
+
aggregated[i] = values.length % 2 === 1
|
|
76
|
+
? values[middle]
|
|
77
|
+
: (values[middle - 1] + values[middle]) / 2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return aggregated;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function blurAlphaMap(alphaMap, size, radius = 0) {
|
|
84
|
+
const blurPasses = Number.isInteger(radius) ? radius : Math.max(0, Math.round(radius || 0));
|
|
85
|
+
if (blurPasses <= 0 || size <= 0) {
|
|
86
|
+
return new Float32Array(alphaMap);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let current = new Float32Array(alphaMap);
|
|
90
|
+
for (let pass = 0; pass < blurPasses; pass++) {
|
|
91
|
+
const next = new Float32Array(current.length);
|
|
92
|
+
for (let y = 0; y < size; y++) {
|
|
93
|
+
for (let x = 0; x < size; x++) {
|
|
94
|
+
let sum = 0;
|
|
95
|
+
let weight = 0;
|
|
96
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
97
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
98
|
+
const xx = x + dx;
|
|
99
|
+
const yy = y + dy;
|
|
100
|
+
if (xx < 0 || yy < 0 || xx >= size || yy >= size) continue;
|
|
101
|
+
const w = dx === 0 && dy === 0 ? 4 : (dx === 0 || dy === 0 ? 2 : 1);
|
|
102
|
+
sum += current[yy * size + xx] * w;
|
|
103
|
+
weight += w;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
next[y * size + x] = clamp01(sum / Math.max(1, weight));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
current = next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return current;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cloneImageData(imageData) {
|
|
116
|
+
return {
|
|
117
|
+
width: imageData.width,
|
|
118
|
+
height: imageData.height,
|
|
119
|
+
data: new Uint8ClampedArray(imageData.data)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function clampChannel(value) {
|
|
124
|
+
if (!Number.isFinite(value)) return 0;
|
|
125
|
+
if (value <= 0) return 0;
|
|
126
|
+
if (value >= 255) return 255;
|
|
127
|
+
return Math.round(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyPreviewWatermark(imageData, alphaMap, position, alphaGain = 1) {
|
|
131
|
+
for (let row = 0; row < position.height; row++) {
|
|
132
|
+
for (let col = 0; col < position.width; col++) {
|
|
133
|
+
const alpha = clamp01(alphaMap[row * position.width + col] * alphaGain);
|
|
134
|
+
if (alpha <= 0.001) continue;
|
|
135
|
+
|
|
136
|
+
const idx = ((position.y + row) * imageData.width + (position.x + col)) * 4;
|
|
137
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
138
|
+
const original = imageData.data[idx + channel];
|
|
139
|
+
imageData.data[idx + channel] = clampChannel(alpha * 255 + (1 - alpha) * original);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function blurImageDataRegionOnce(imageData, position) {
|
|
146
|
+
const blurred = cloneImageData(imageData);
|
|
147
|
+
|
|
148
|
+
for (let row = 0; row < position.height; row++) {
|
|
149
|
+
for (let col = 0; col < position.width; col++) {
|
|
150
|
+
let sumR = 0;
|
|
151
|
+
let sumG = 0;
|
|
152
|
+
let sumB = 0;
|
|
153
|
+
let weight = 0;
|
|
154
|
+
|
|
155
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
156
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
157
|
+
const localX = Math.max(0, Math.min(position.width - 1, col + dx));
|
|
158
|
+
const localY = Math.max(0, Math.min(position.height - 1, row + dy));
|
|
159
|
+
const idx = ((position.y + localY) * imageData.width + (position.x + localX)) * 4;
|
|
160
|
+
const w = dx === 0 && dy === 0 ? 4 : (dx === 0 || dy === 0 ? 2 : 1);
|
|
161
|
+
sumR += imageData.data[idx] * w;
|
|
162
|
+
sumG += imageData.data[idx + 1] * w;
|
|
163
|
+
sumB += imageData.data[idx + 2] * w;
|
|
164
|
+
weight += w;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const outIdx = ((position.y + row) * blurred.width + (position.x + col)) * 4;
|
|
169
|
+
blurred.data[outIdx] = clampChannel(sumR / weight);
|
|
170
|
+
blurred.data[outIdx + 1] = clampChannel(sumG / weight);
|
|
171
|
+
blurred.data[outIdx + 2] = clampChannel(sumB / weight);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return blurred;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function blurImageDataRegion(imageData, position, radius = 0) {
|
|
179
|
+
const blurPasses = Number.isInteger(radius) ? radius : Math.max(0, Math.round(radius || 0));
|
|
180
|
+
if (blurPasses <= 0) {
|
|
181
|
+
return cloneImageData(imageData);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let current = cloneImageData(imageData);
|
|
185
|
+
for (let pass = 0; pass < blurPasses; pass++) {
|
|
186
|
+
current = blurImageDataRegionOnce(current, position);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return current;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function averageStripColor(imageData, {
|
|
193
|
+
xFrom,
|
|
194
|
+
xTo,
|
|
195
|
+
yFrom,
|
|
196
|
+
yTo
|
|
197
|
+
}) {
|
|
198
|
+
let sumR = 0;
|
|
199
|
+
let sumG = 0;
|
|
200
|
+
let sumB = 0;
|
|
201
|
+
let count = 0;
|
|
202
|
+
|
|
203
|
+
const minX = Math.max(0, Math.min(xFrom, xTo));
|
|
204
|
+
const maxX = Math.min(imageData.width - 1, Math.max(xFrom, xTo));
|
|
205
|
+
const minY = Math.max(0, Math.min(yFrom, yTo));
|
|
206
|
+
const maxY = Math.min(imageData.height - 1, Math.max(yFrom, yTo));
|
|
207
|
+
|
|
208
|
+
for (let y = minY; y <= maxY; y++) {
|
|
209
|
+
for (let x = minX; x <= maxX; x++) {
|
|
210
|
+
const idx = (y * imageData.width + x) * 4;
|
|
211
|
+
sumR += imageData.data[idx];
|
|
212
|
+
sumG += imageData.data[idx + 1];
|
|
213
|
+
sumB += imageData.data[idx + 2];
|
|
214
|
+
count++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (count <= 0) {
|
|
219
|
+
return [0, 0, 0];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return [sumR / count, sumG / count, sumB / count];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function lerpColor(left, right, t) {
|
|
226
|
+
return [
|
|
227
|
+
left[0] * (1 - t) + right[0] * t,
|
|
228
|
+
left[1] * (1 - t) + right[1] * t,
|
|
229
|
+
left[2] * (1 - t) + right[2] * t
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function measureRegionAbsDelta(candidateImageData, targetImageData, position) {
|
|
234
|
+
let total = 0;
|
|
235
|
+
let count = 0;
|
|
236
|
+
for (let row = 0; row < position.height; row++) {
|
|
237
|
+
for (let col = 0; col < position.width; col++) {
|
|
238
|
+
const idx = ((position.y + row) * candidateImageData.width + (position.x + col)) * 4;
|
|
239
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
240
|
+
total += Math.abs(candidateImageData.data[idx + channel] - targetImageData.data[idx + channel]);
|
|
241
|
+
count++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return count > 0 ? total / count : 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function averageSampleColor(imageData, samples) {
|
|
250
|
+
let sumR = 0;
|
|
251
|
+
let sumG = 0;
|
|
252
|
+
let sumB = 0;
|
|
253
|
+
let count = 0;
|
|
254
|
+
|
|
255
|
+
for (const [x, y] of samples) {
|
|
256
|
+
if (x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const idx = (y * imageData.width + x) * 4;
|
|
260
|
+
sumR += imageData.data[idx];
|
|
261
|
+
sumG += imageData.data[idx + 1];
|
|
262
|
+
sumB += imageData.data[idx + 2];
|
|
263
|
+
count++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (count <= 0) {
|
|
267
|
+
return [0, 0, 0];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return [sumR / count, sumG / count, sumB / count];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function measurePreviewBoundaryMetrics(candidateImageData, previewImageData, position) {
|
|
274
|
+
let rawTotal = 0;
|
|
275
|
+
let previewBoundaryTotal = 0;
|
|
276
|
+
let localContrastTotal = 0;
|
|
277
|
+
let count = 0;
|
|
278
|
+
|
|
279
|
+
const compareBoundaryPixel = (x, y, insideSamples, outsideSamples) => {
|
|
280
|
+
const idx = (y * candidateImageData.width + x) * 4;
|
|
281
|
+
const previewIdx = (y * previewImageData.width + x) * 4;
|
|
282
|
+
const inside = averageSampleColor(previewImageData, insideSamples);
|
|
283
|
+
const outside = averageSampleColor(previewImageData, outsideSamples);
|
|
284
|
+
|
|
285
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
286
|
+
rawTotal += Math.abs(candidateImageData.data[idx + channel] - outside[channel]);
|
|
287
|
+
previewBoundaryTotal += Math.abs(previewImageData.data[previewIdx + channel] - outside[channel]);
|
|
288
|
+
localContrastTotal += Math.abs(inside[channel] - outside[channel]);
|
|
289
|
+
count++;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
for (let col = 0; col < position.width; col++) {
|
|
294
|
+
const x = position.x + col;
|
|
295
|
+
compareBoundaryPixel(
|
|
296
|
+
x,
|
|
297
|
+
position.y,
|
|
298
|
+
[
|
|
299
|
+
[x - 1, position.y],
|
|
300
|
+
[x, position.y],
|
|
301
|
+
[x + 1, position.y]
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
[x - 1, position.y - 1],
|
|
305
|
+
[x, position.y - 1],
|
|
306
|
+
[x + 1, position.y - 1]
|
|
307
|
+
]
|
|
308
|
+
);
|
|
309
|
+
compareBoundaryPixel(
|
|
310
|
+
x,
|
|
311
|
+
position.y + position.height - 1,
|
|
312
|
+
[
|
|
313
|
+
[x - 1, position.y + position.height - 1],
|
|
314
|
+
[x, position.y + position.height - 1],
|
|
315
|
+
[x + 1, position.y + position.height - 1]
|
|
316
|
+
],
|
|
317
|
+
[
|
|
318
|
+
[x - 1, position.y + position.height],
|
|
319
|
+
[x, position.y + position.height],
|
|
320
|
+
[x + 1, position.y + position.height]
|
|
321
|
+
]
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (let row = 1; row < position.height - 1; row++) {
|
|
326
|
+
const y = position.y + row;
|
|
327
|
+
compareBoundaryPixel(
|
|
328
|
+
position.x,
|
|
329
|
+
y,
|
|
330
|
+
[
|
|
331
|
+
[position.x, y - 1],
|
|
332
|
+
[position.x, y],
|
|
333
|
+
[position.x, y + 1]
|
|
334
|
+
],
|
|
335
|
+
[
|
|
336
|
+
[position.x - 1, y - 1],
|
|
337
|
+
[position.x - 1, y],
|
|
338
|
+
[position.x - 1, y + 1]
|
|
339
|
+
]
|
|
340
|
+
);
|
|
341
|
+
compareBoundaryPixel(
|
|
342
|
+
position.x + position.width - 1,
|
|
343
|
+
y,
|
|
344
|
+
[
|
|
345
|
+
[position.x + position.width - 1, y - 1],
|
|
346
|
+
[position.x + position.width - 1, y],
|
|
347
|
+
[position.x + position.width - 1, y + 1]
|
|
348
|
+
],
|
|
349
|
+
[
|
|
350
|
+
[position.x + position.width, y - 1],
|
|
351
|
+
[position.x + position.width, y],
|
|
352
|
+
[position.x + position.width, y + 1]
|
|
353
|
+
]
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const rawScore = count > 0 ? rawTotal / count : 0;
|
|
358
|
+
const previewBoundaryScore = count > 0 ? previewBoundaryTotal / count : 0;
|
|
359
|
+
const localContrastScore = count > 0 ? localContrastTotal / count : 0;
|
|
360
|
+
const normalizer = Math.max(1, previewBoundaryScore, localContrastScore);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
rawScore,
|
|
364
|
+
previewBoundaryScore,
|
|
365
|
+
localContrastScore,
|
|
366
|
+
normalizer,
|
|
367
|
+
normalizedScore: rawScore / normalizer
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function measurePreviewBoundaryContinuity(candidateImageData, previewImageData, position) {
|
|
372
|
+
return measurePreviewBoundaryMetrics(candidateImageData, previewImageData, position).normalizedScore;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function buildPreviewNeighborhoodPrior({
|
|
376
|
+
previewImageData,
|
|
377
|
+
position,
|
|
378
|
+
radius = 6
|
|
379
|
+
}) {
|
|
380
|
+
if (!previewImageData || !position) {
|
|
381
|
+
throw new TypeError('buildPreviewNeighborhoodPrior requires previewImageData and position');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const stripRadius = Math.max(1, Math.round(radius || 1));
|
|
385
|
+
const prior = cloneImageData(previewImageData);
|
|
386
|
+
const leftBoundary = [];
|
|
387
|
+
const rightBoundary = [];
|
|
388
|
+
const topBoundary = [];
|
|
389
|
+
const bottomBoundary = [];
|
|
390
|
+
|
|
391
|
+
for (let row = 0; row < position.height; row++) {
|
|
392
|
+
const y = position.y + row;
|
|
393
|
+
leftBoundary.push(averageStripColor(previewImageData, {
|
|
394
|
+
xFrom: position.x - stripRadius,
|
|
395
|
+
xTo: position.x - 1,
|
|
396
|
+
yFrom: y - 1,
|
|
397
|
+
yTo: y + 1
|
|
398
|
+
}));
|
|
399
|
+
rightBoundary.push(averageStripColor(previewImageData, {
|
|
400
|
+
xFrom: position.x + position.width,
|
|
401
|
+
xTo: position.x + position.width + stripRadius - 1,
|
|
402
|
+
yFrom: y - 1,
|
|
403
|
+
yTo: y + 1
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (let col = 0; col < position.width; col++) {
|
|
408
|
+
const x = position.x + col;
|
|
409
|
+
topBoundary.push(averageStripColor(previewImageData, {
|
|
410
|
+
xFrom: x - 1,
|
|
411
|
+
xTo: x + 1,
|
|
412
|
+
yFrom: position.y - stripRadius,
|
|
413
|
+
yTo: position.y - 1
|
|
414
|
+
}));
|
|
415
|
+
bottomBoundary.push(averageStripColor(previewImageData, {
|
|
416
|
+
xFrom: x - 1,
|
|
417
|
+
xTo: x + 1,
|
|
418
|
+
yFrom: position.y + position.height,
|
|
419
|
+
yTo: position.y + position.height + stripRadius - 1
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (let row = 0; row < position.height; row++) {
|
|
424
|
+
const ty = position.height <= 1 ? 0.5 : row / (position.height - 1);
|
|
425
|
+
for (let col = 0; col < position.width; col++) {
|
|
426
|
+
const tx = position.width <= 1 ? 0.5 : col / (position.width - 1);
|
|
427
|
+
const horizontal = lerpColor(leftBoundary[row], rightBoundary[row], tx);
|
|
428
|
+
const vertical = lerpColor(topBoundary[col], bottomBoundary[col], ty);
|
|
429
|
+
const idx = ((position.y + row) * prior.width + (position.x + col)) * 4;
|
|
430
|
+
prior.data[idx] = clampChannel((horizontal[0] + vertical[0]) * 0.5);
|
|
431
|
+
prior.data[idx + 1] = clampChannel((horizontal[1] + vertical[1]) * 0.5);
|
|
432
|
+
prior.data[idx + 2] = clampChannel((horizontal[2] + vertical[2]) * 0.5);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (position.width <= 1 || position.height <= 1) {
|
|
437
|
+
return prior;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const relaxationPasses = Math.max(24, Math.round((position.width + position.height) * 2));
|
|
441
|
+
for (let pass = 0; pass < relaxationPasses; pass++) {
|
|
442
|
+
for (let row = 0; row < position.height; row++) {
|
|
443
|
+
const y = position.y + row;
|
|
444
|
+
for (let col = 0; col < position.width; col++) {
|
|
445
|
+
const x = position.x + col;
|
|
446
|
+
const idx = (y * prior.width + x) * 4;
|
|
447
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
448
|
+
let sum = 0;
|
|
449
|
+
let weight = 0;
|
|
450
|
+
const neighbors = [
|
|
451
|
+
[x - 1, y, 1],
|
|
452
|
+
[x + 1, y, 1],
|
|
453
|
+
[x, y - 1, 1],
|
|
454
|
+
[x, y + 1, 1],
|
|
455
|
+
[x - 1, y - 1, 0.5],
|
|
456
|
+
[x + 1, y - 1, 0.5],
|
|
457
|
+
[x - 1, y + 1, 0.5],
|
|
458
|
+
[x + 1, y + 1, 0.5]
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
for (const [neighborX, neighborY, neighborWeight] of neighbors) {
|
|
462
|
+
if (
|
|
463
|
+
neighborX < 0 ||
|
|
464
|
+
neighborY < 0 ||
|
|
465
|
+
neighborX >= prior.width ||
|
|
466
|
+
neighborY >= prior.height
|
|
467
|
+
) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const neighborIdx = (neighborY * prior.width + neighborX) * 4;
|
|
472
|
+
sum += prior.data[neighborIdx + channel] * neighborWeight;
|
|
473
|
+
weight += neighborWeight;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
prior.data[idx + channel] = clampChannel(sum / Math.max(1, weight));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return prior;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function renderPreviewWatermarkObservation({
|
|
486
|
+
sourceImageData,
|
|
487
|
+
alphaMap,
|
|
488
|
+
position,
|
|
489
|
+
alphaGain = 1,
|
|
490
|
+
compositeBlurRadius = 0
|
|
491
|
+
}) {
|
|
492
|
+
if (!sourceImageData || !alphaMap || !position) {
|
|
493
|
+
throw new TypeError('renderPreviewWatermarkObservation requires sourceImageData, alphaMap, and position');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const rendered = cloneImageData(sourceImageData);
|
|
497
|
+
applyPreviewWatermark(rendered, alphaMap, position, alphaGain);
|
|
498
|
+
return blurImageDataRegion(rendered, position, compositeBlurRadius);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function fitConstrainedPreviewAlphaModel({
|
|
502
|
+
sourceImageData,
|
|
503
|
+
previewImageData,
|
|
504
|
+
standardAlphaMap,
|
|
505
|
+
position,
|
|
506
|
+
shiftCandidates = [-0.5, 0, 0.5],
|
|
507
|
+
scaleCandidates = [0.99, 1, 1.01],
|
|
508
|
+
blurRadii = [0, 1],
|
|
509
|
+
alphaGainCandidates = [1]
|
|
510
|
+
}) {
|
|
511
|
+
if (!sourceImageData || !previewImageData || !standardAlphaMap || !position) {
|
|
512
|
+
throw new TypeError('fitConstrainedPreviewAlphaModel requires sourceImageData, previewImageData, standardAlphaMap, and position');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const size = position.width;
|
|
516
|
+
if (!size || size !== position.height || standardAlphaMap.length !== size * size) {
|
|
517
|
+
throw new RangeError('fitConstrainedPreviewAlphaModel requires a square ROI and matching standardAlphaMap size');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let best = null;
|
|
521
|
+
for (const scale of scaleCandidates) {
|
|
522
|
+
for (const dy of shiftCandidates) {
|
|
523
|
+
for (const dx of shiftCandidates) {
|
|
524
|
+
const warped = warpAlphaMap(standardAlphaMap, size, { dx, dy, scale });
|
|
525
|
+
for (const blurRadius of blurRadii) {
|
|
526
|
+
const alphaMap = blurAlphaMap(warped, size, blurRadius);
|
|
527
|
+
for (const alphaGain of alphaGainCandidates) {
|
|
528
|
+
const restored = cloneImageData(previewImageData);
|
|
529
|
+
removeWatermark(restored, alphaMap, position, { alphaGain });
|
|
530
|
+
const score = measureRegionAbsDelta(restored, sourceImageData, position);
|
|
531
|
+
|
|
532
|
+
if (!best || score < best.score) {
|
|
533
|
+
best = {
|
|
534
|
+
alphaMap,
|
|
535
|
+
alphaGain,
|
|
536
|
+
params: {
|
|
537
|
+
shift: { dx, dy, scale },
|
|
538
|
+
blurRadius
|
|
539
|
+
},
|
|
540
|
+
score
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return best;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function fitPreviewRenderModel({
|
|
553
|
+
sourceImageData,
|
|
554
|
+
previewImageData,
|
|
555
|
+
standardAlphaMap,
|
|
556
|
+
position,
|
|
557
|
+
shiftCandidates = [-0.5, 0, 0.5],
|
|
558
|
+
scaleCandidates = [0.99, 1, 1.01],
|
|
559
|
+
alphaBlurRadii = [0, 1],
|
|
560
|
+
compositeBlurRadii = [0, 1],
|
|
561
|
+
alphaGainCandidates = [1]
|
|
562
|
+
}) {
|
|
563
|
+
if (!sourceImageData || !previewImageData || !standardAlphaMap || !position) {
|
|
564
|
+
throw new TypeError('fitPreviewRenderModel requires sourceImageData, previewImageData, standardAlphaMap, and position');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const size = position.width;
|
|
568
|
+
if (!size || size !== position.height || standardAlphaMap.length !== size * size) {
|
|
569
|
+
throw new RangeError('fitPreviewRenderModel requires a square ROI and matching standardAlphaMap size');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
let best = null;
|
|
573
|
+
for (const scale of scaleCandidates) {
|
|
574
|
+
for (const dy of shiftCandidates) {
|
|
575
|
+
for (const dx of shiftCandidates) {
|
|
576
|
+
const warped = warpAlphaMap(standardAlphaMap, size, { dx, dy, scale });
|
|
577
|
+
for (const alphaBlurRadius of alphaBlurRadii) {
|
|
578
|
+
const alphaMap = blurAlphaMap(warped, size, alphaBlurRadius);
|
|
579
|
+
for (const compositeBlurRadius of compositeBlurRadii) {
|
|
580
|
+
for (const alphaGain of alphaGainCandidates) {
|
|
581
|
+
const rendered = renderPreviewWatermarkObservation({
|
|
582
|
+
sourceImageData,
|
|
583
|
+
alphaMap,
|
|
584
|
+
position,
|
|
585
|
+
alphaGain,
|
|
586
|
+
compositeBlurRadius
|
|
587
|
+
});
|
|
588
|
+
const score = measureRegionAbsDelta(rendered, previewImageData, position);
|
|
589
|
+
|
|
590
|
+
if (!best || score < best.score) {
|
|
591
|
+
best = {
|
|
592
|
+
alphaMap,
|
|
593
|
+
alphaGain,
|
|
594
|
+
params: {
|
|
595
|
+
shift: { dx, dy, scale },
|
|
596
|
+
alphaBlurRadius,
|
|
597
|
+
compositeBlurRadius
|
|
598
|
+
},
|
|
599
|
+
score
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return best;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function fitPreviewOnlyRenderModel({
|
|
613
|
+
previewImageData,
|
|
614
|
+
standardAlphaMap,
|
|
615
|
+
position,
|
|
616
|
+
shiftCandidates = [-0.5, 0, 0.5],
|
|
617
|
+
scaleCandidates = [0.99, 1, 1.01],
|
|
618
|
+
alphaBlurRadii = [0, 1],
|
|
619
|
+
compositeBlurRadii = [0, 1],
|
|
620
|
+
alphaGainCandidates = [1],
|
|
621
|
+
blendStrengthCandidates = [0.85],
|
|
622
|
+
priorRadiusCandidates = null,
|
|
623
|
+
priorRadius = 6,
|
|
624
|
+
boundaryContinuityWeight = 0
|
|
625
|
+
}) {
|
|
626
|
+
if (!previewImageData || !standardAlphaMap || !position) {
|
|
627
|
+
throw new TypeError('fitPreviewOnlyRenderModel requires previewImageData, standardAlphaMap, and position');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const size = position.width;
|
|
631
|
+
if (!size || size !== position.height || standardAlphaMap.length !== size * size) {
|
|
632
|
+
throw new RangeError('fitPreviewOnlyRenderModel requires a square ROI and matching standardAlphaMap size');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const resolvedPriorRadiusCandidates = Array.isArray(priorRadiusCandidates) && priorRadiusCandidates.length > 0
|
|
636
|
+
? priorRadiusCandidates
|
|
637
|
+
: [priorRadius];
|
|
638
|
+
|
|
639
|
+
const alphaCandidates = [];
|
|
640
|
+
for (const candidatePriorRadius of resolvedPriorRadiusCandidates) {
|
|
641
|
+
const priorImageData = buildPreviewNeighborhoodPrior({
|
|
642
|
+
previewImageData,
|
|
643
|
+
position,
|
|
644
|
+
radius: candidatePriorRadius
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
let alphaBestForRadius = null;
|
|
648
|
+
for (const scale of scaleCandidates) {
|
|
649
|
+
for (const dy of shiftCandidates) {
|
|
650
|
+
for (const dx of shiftCandidates) {
|
|
651
|
+
const warped = warpAlphaMap(standardAlphaMap, size, { dx, dy, scale });
|
|
652
|
+
for (const alphaBlurRadius of alphaBlurRadii) {
|
|
653
|
+
const alphaMap = blurAlphaMap(warped, size, alphaBlurRadius);
|
|
654
|
+
for (const compositeBlurRadius of compositeBlurRadii) {
|
|
655
|
+
for (const alphaGain of alphaGainCandidates) {
|
|
656
|
+
const rendered = renderPreviewWatermarkObservation({
|
|
657
|
+
sourceImageData: priorImageData,
|
|
658
|
+
alphaMap,
|
|
659
|
+
position,
|
|
660
|
+
alphaGain,
|
|
661
|
+
compositeBlurRadius
|
|
662
|
+
});
|
|
663
|
+
const forwardScore = measureRegionAbsDelta(rendered, previewImageData, position);
|
|
664
|
+
|
|
665
|
+
if (!alphaBestForRadius || forwardScore < alphaBestForRadius.forwardScore) {
|
|
666
|
+
alphaBestForRadius = {
|
|
667
|
+
alphaMap,
|
|
668
|
+
alphaGain,
|
|
669
|
+
priorImageData,
|
|
670
|
+
params: {
|
|
671
|
+
shift: { dx, dy, scale },
|
|
672
|
+
alphaBlurRadius,
|
|
673
|
+
compositeBlurRadius,
|
|
674
|
+
priorRadius: candidatePriorRadius
|
|
675
|
+
},
|
|
676
|
+
forwardScore
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (alphaBestForRadius) {
|
|
687
|
+
alphaCandidates.push(alphaBestForRadius);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let best = null;
|
|
692
|
+
for (const alphaCandidate of alphaCandidates) {
|
|
693
|
+
for (const blendStrength of blendStrengthCandidates) {
|
|
694
|
+
const restored = restorePreviewRegionWithNeighborhoodPrior({
|
|
695
|
+
previewImageData,
|
|
696
|
+
alphaMap: alphaCandidate.alphaMap,
|
|
697
|
+
position,
|
|
698
|
+
alphaGain: alphaCandidate.alphaGain,
|
|
699
|
+
priorImageData: alphaCandidate.priorImageData,
|
|
700
|
+
blendStrength
|
|
701
|
+
});
|
|
702
|
+
const rerendered = renderPreviewWatermarkObservation({
|
|
703
|
+
sourceImageData: restored,
|
|
704
|
+
alphaMap: alphaCandidate.alphaMap,
|
|
705
|
+
position,
|
|
706
|
+
alphaGain: alphaCandidate.alphaGain,
|
|
707
|
+
compositeBlurRadius: alphaCandidate.params.compositeBlurRadius
|
|
708
|
+
});
|
|
709
|
+
const inverseScore = measureRegionAbsDelta(rerendered, previewImageData, position);
|
|
710
|
+
const boundaryMetrics = boundaryContinuityWeight > 0
|
|
711
|
+
? measurePreviewBoundaryMetrics(restored, previewImageData, position)
|
|
712
|
+
: {
|
|
713
|
+
rawScore: 0,
|
|
714
|
+
previewBoundaryScore: 0,
|
|
715
|
+
localContrastScore: 0,
|
|
716
|
+
normalizer: 1,
|
|
717
|
+
normalizedScore: 0
|
|
718
|
+
};
|
|
719
|
+
const boundaryScore = boundaryMetrics.normalizedScore;
|
|
720
|
+
const score = inverseScore + boundaryScore * boundaryContinuityWeight;
|
|
721
|
+
|
|
722
|
+
if (!best || score < best.score) {
|
|
723
|
+
best = {
|
|
724
|
+
alphaMap: alphaCandidate.alphaMap,
|
|
725
|
+
alphaGain: alphaCandidate.alphaGain,
|
|
726
|
+
priorImageData: alphaCandidate.priorImageData,
|
|
727
|
+
params: {
|
|
728
|
+
...alphaCandidate.params,
|
|
729
|
+
blendStrength
|
|
730
|
+
},
|
|
731
|
+
score,
|
|
732
|
+
diagnostics: {
|
|
733
|
+
forwardScore: alphaCandidate.forwardScore,
|
|
734
|
+
inverseScore,
|
|
735
|
+
boundaryScore,
|
|
736
|
+
boundaryRawScore: boundaryMetrics.rawScore,
|
|
737
|
+
boundaryPreviewScore: boundaryMetrics.previewBoundaryScore,
|
|
738
|
+
boundaryContrastScore: boundaryMetrics.localContrastScore,
|
|
739
|
+
boundaryNormalizer: boundaryMetrics.normalizer
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return best;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function restorePreviewRegionWithRenderModel({
|
|
750
|
+
previewImageData,
|
|
751
|
+
alphaMap,
|
|
752
|
+
position,
|
|
753
|
+
alphaGain = 1,
|
|
754
|
+
compositeBlurRadius = 0,
|
|
755
|
+
iterations = 12,
|
|
756
|
+
stepSize = 0.85
|
|
757
|
+
}) {
|
|
758
|
+
if (!previewImageData || !alphaMap || !position) {
|
|
759
|
+
throw new TypeError('restorePreviewRegionWithRenderModel requires previewImageData, alphaMap, and position');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let deblurred = cloneImageData(previewImageData);
|
|
763
|
+
const totalIterations = Math.max(0, Math.round(iterations || 0));
|
|
764
|
+
const resolvedStepSize = Number.isFinite(stepSize) ? stepSize : 0.85;
|
|
765
|
+
|
|
766
|
+
if (compositeBlurRadius > 0 && totalIterations > 0) {
|
|
767
|
+
for (let iteration = 0; iteration < totalIterations; iteration++) {
|
|
768
|
+
const reblurred = blurImageDataRegion(deblurred, position, compositeBlurRadius);
|
|
769
|
+
|
|
770
|
+
for (let row = 0; row < position.height; row++) {
|
|
771
|
+
for (let col = 0; col < position.width; col++) {
|
|
772
|
+
const idx = ((position.y + row) * deblurred.width + (position.x + col)) * 4;
|
|
773
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
774
|
+
const error = previewImageData.data[idx + channel] - reblurred.data[idx + channel];
|
|
775
|
+
deblurred.data[idx + channel] = clampChannel(
|
|
776
|
+
deblurred.data[idx + channel] + error * resolvedStepSize
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const restored = cloneImageData(deblurred);
|
|
785
|
+
removeWatermark(restored, alphaMap, position, { alphaGain });
|
|
786
|
+
return restored;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function restorePreviewRegionWithNeighborhoodPrior({
|
|
790
|
+
previewImageData,
|
|
791
|
+
alphaMap,
|
|
792
|
+
position,
|
|
793
|
+
alphaGain = 1,
|
|
794
|
+
priorImageData,
|
|
795
|
+
blendStrength = 0.85
|
|
796
|
+
}) {
|
|
797
|
+
if (!previewImageData || !alphaMap || !position || !priorImageData) {
|
|
798
|
+
throw new TypeError('restorePreviewRegionWithNeighborhoodPrior requires previewImageData, alphaMap, position, and priorImageData');
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const restored = cloneImageData(previewImageData);
|
|
802
|
+
removeWatermark(restored, alphaMap, position, { alphaGain });
|
|
803
|
+
|
|
804
|
+
const resolvedBlendStrength = Number.isFinite(blendStrength) ? blendStrength : 0.85;
|
|
805
|
+
for (let row = 0; row < position.height; row++) {
|
|
806
|
+
for (let col = 0; col < position.width; col++) {
|
|
807
|
+
const alpha = clamp01(alphaMap[row * position.width + col] * alphaGain);
|
|
808
|
+
if (alpha <= 0.001) continue;
|
|
809
|
+
|
|
810
|
+
const blend = Math.max(0, Math.min(1, Math.sqrt(alpha) * resolvedBlendStrength));
|
|
811
|
+
const idx = ((position.y + row) * restored.width + (position.x + col)) * 4;
|
|
812
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
813
|
+
restored.data[idx + channel] = clampChannel(
|
|
814
|
+
restored.data[idx + channel] * (1 - blend) +
|
|
815
|
+
priorImageData.data[idx + channel] * blend
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return restored;
|
|
822
|
+
}
|