@pilio/gemini-watermark-remover 1.0.23 → 1.0.25

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pilio/gemini-watermark-remover",
3
3
  "description": "Automatically removes watermarks from Gemini AI generated images",
4
- "version": "1.0.23",
4
+ "version": "1.0.25",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -96,7 +96,8 @@
96
96
  "release:readiness": "node scripts/create-release-readiness-report.js",
97
97
  "release:quality-gate": "pnpm compare:allenk-v2 -- --fail-on-incomplete && pnpm release:readiness -- --fail-on-not-ready",
98
98
  "release:goal-audit": "node scripts/create-release-goal-audit-report.js",
99
- "release:preflight": "pnpm test && pnpm build && pnpm package:extension && pnpm release:quality-gate && pnpm release:goal-audit -- --fail-on-incomplete",
99
+ "release:ci-check": "node scripts/check-github-ci.js --workflow ci.yml --commit HEAD --fail-closed",
100
+ "release:preflight": "pnpm test && pnpm build && pnpm package:extension && pnpm release:quality-gate && pnpm release:goal-audit -- --fail-on-incomplete && pnpm release:ci-check",
100
101
  "visible-residual:loop": "node scripts/run-visible-residual-loop.js",
101
102
  "visible-residual:verify": "node scripts/verify-visible-residual-loop.js",
102
103
  "visible-residual:validate-human-review": "node scripts/validate-visible-residual-human-review.js",
@@ -112,6 +113,9 @@
112
113
  "render:skipped-crops": "node scripts/render-skipped-candidate-crops.js",
113
114
  "render:video-crops": "node scripts/render-video-crop-sheet.js",
114
115
  "analyze:video-residual": "node scripts/analyze-video-residual.js",
116
+ "score:video-candidates": "node scripts/score-video-watermark-candidates.js",
117
+ "sweep:veo-text-cleanup": "node scripts/sweep-veo-text-cleanup.js",
118
+ "score:video-output-residual": "node scripts/score-video-output-residual.js",
115
119
  "benchmark:video-crops": "node scripts/video-crop-benchmark.js",
116
120
  "report:video-crops": "node scripts/report-video-crop-benchmark.js",
117
121
  "diagnose:video-lowbody": "node scripts/diagnose-video-lowbody-regression.js",
@@ -144,7 +148,12 @@
144
148
  "export:allenk-fdncnn-onnx-video": "node scripts/export-allenk-fdncnn-onnx-video.js",
145
149
  "export:allenk-fdncnn-onnx-frame-video": "node scripts/export-allenk-fdncnn-onnx-frame-video.js",
146
150
  "report:allenk-fdncnn-onnx-video-evidence": "node scripts/create-allenk-fdncnn-onnx-video-evidence.js",
151
+ "report:allenk-catalog-audit": "node scripts/create-allenk-catalog-audit.js",
152
+ "report:allenk-video-binary-analysis": "node scripts/create-allenk-video-binary-analysis.js",
147
153
  "export:video-ui-preset": "node scripts/export-video-ui-preset.js",
154
+ "verify:video-ui-preset-output": "node scripts/verify-video-ui-preset-output.js",
155
+ "verify:video-ui-preset-batch": "node scripts/verify-video-ui-preset-batch.js",
156
+ "verify:video-regression-samples": "pnpm verify:video-ui-preset-batch -- --manifest tests/fixtures/video-regression-samples/gemini-video-regression-samples.json --output-dir .artifacts/video-regression-samples --no-screenshots --fail-on-residual",
148
157
  "render:video-comparison-grid": "node scripts/render-video-comparison-grid.js",
149
158
  "package:extension": "node scripts/package-extension-release.js",
150
159
  "cli:smoke": "node bin/gwr.mjs --help",
@@ -1,5 +1,3 @@
1
- import { createAlphaGradientMask } from './alphaGradientMask.js';
2
-
3
1
  const ALLENK_FDNCNN_MODEL = Object.freeze({
4
2
  name: 'FDnCNN Color FP16',
5
3
  upstream: 'allenk/GeminiWatermarkTool',
@@ -20,6 +18,15 @@ function clamp(value, min, max) {
20
18
  return Math.max(min, Math.min(max, value));
21
19
  }
22
20
 
21
+ function reflect101(index, length) {
22
+ if (length <= 1) return 0;
23
+ let value = Math.round(index);
24
+ while (value < 0 || value >= length) {
25
+ value = value < 0 ? -value : (length * 2 - value - 2);
26
+ }
27
+ return value;
28
+ }
29
+
23
30
  function createGaussianKernel(sigma, radius = Math.ceil(sigma * 3)) {
24
31
  const safeSigma = Math.max(0.01, sigma);
25
32
  const safeRadius = Math.max(1, Math.round(radius));
@@ -52,7 +59,7 @@ function gaussianBlurFloatMap(source, width, height, sigma, radius = Math.ceil(s
52
59
  for (let x = 0; x < width; x++) {
53
60
  let sum = 0;
54
61
  for (let dx = -r; dx <= r; dx++) {
55
- const xx = clamp(x + dx, 0, width - 1);
62
+ const xx = reflect101(x + dx, width);
56
63
  sum += source[y * width + xx] * kernel[dx + r];
57
64
  }
58
65
  temp[y * width + x] = sum;
@@ -63,7 +70,7 @@ function gaussianBlurFloatMap(source, width, height, sigma, radius = Math.ceil(s
63
70
  for (let x = 0; x < width; x++) {
64
71
  let sum = 0;
65
72
  for (let dy = -r; dy <= r; dy++) {
66
- const yy = clamp(y + dy, 0, height - 1);
73
+ const yy = reflect101(y + dy, height);
67
74
  sum += temp[yy * width + x] * kernel[dy + r];
68
75
  }
69
76
  output[y * width + x] = sum;
@@ -123,6 +130,139 @@ function resizeSquareAlphaMapArea(sourceAlpha, sourceSize, targetWidth, targetHe
123
130
  return output;
124
131
  }
125
132
 
133
+ function resizeSquareAlphaMapLinear(sourceAlpha, sourceSize, targetWidth, targetHeight = targetWidth) {
134
+ if (!sourceAlpha || sourceSize <= 0 || targetWidth <= 0 || targetHeight <= 0) {
135
+ return new Float32Array(0);
136
+ }
137
+ if (sourceSize === targetWidth && sourceSize === targetHeight) {
138
+ return new Float32Array(sourceAlpha);
139
+ }
140
+
141
+ const output = new Float32Array(targetWidth * targetHeight);
142
+ const scaleX = sourceSize / targetWidth;
143
+ const scaleY = sourceSize / targetHeight;
144
+
145
+ for (let y = 0; y < targetHeight; y++) {
146
+ const sourceY = (y + 0.5) * scaleY - 0.5;
147
+ const y0 = Math.floor(sourceY);
148
+ const y1 = y0 + 1;
149
+ const wy = sourceY - y0;
150
+
151
+ for (let x = 0; x < targetWidth; x++) {
152
+ const sourceX = (x + 0.5) * scaleX - 0.5;
153
+ const x0 = Math.floor(sourceX);
154
+ const x1 = x0 + 1;
155
+ const wx = sourceX - x0;
156
+
157
+ const topLeft = sourceAlpha[clamp(y0, 0, sourceSize - 1) * sourceSize + clamp(x0, 0, sourceSize - 1)] || 0;
158
+ const topRight = sourceAlpha[clamp(y0, 0, sourceSize - 1) * sourceSize + clamp(x1, 0, sourceSize - 1)] || 0;
159
+ const bottomLeft = sourceAlpha[clamp(y1, 0, sourceSize - 1) * sourceSize + clamp(x0, 0, sourceSize - 1)] || 0;
160
+ const bottomRight = sourceAlpha[clamp(y1, 0, sourceSize - 1) * sourceSize + clamp(x1, 0, sourceSize - 1)] || 0;
161
+ const top = topLeft * (1 - wx) + topRight * wx;
162
+ const bottom = bottomLeft * (1 - wx) + bottomRight * wx;
163
+
164
+ output[y * targetWidth + x] = top * (1 - wy) + bottom * wy;
165
+ }
166
+ }
167
+
168
+ return output;
169
+ }
170
+
171
+ function resizeSquareAlphaMapOpenCv(sourceAlpha, sourceSize, targetWidth, targetHeight = targetWidth) {
172
+ return targetWidth > sourceSize
173
+ ? resizeSquareAlphaMapLinear(sourceAlpha, sourceSize, targetWidth, targetHeight)
174
+ : resizeSquareAlphaMapArea(sourceAlpha, sourceSize, targetWidth, targetHeight);
175
+ }
176
+
177
+ const ALLENK_ELLIPSE_5X5 = Object.freeze([
178
+ [0, 0, 1, 0, 0],
179
+ [1, 1, 1, 1, 1],
180
+ [1, 1, 1, 1, 1],
181
+ [1, 1, 1, 1, 1],
182
+ [0, 0, 1, 0, 0]
183
+ ]);
184
+
185
+ function dilateAllenkEllipse5x5(values, width, height) {
186
+ const output = new Float32Array(values.length);
187
+
188
+ for (let y = 0; y < height; y++) {
189
+ for (let x = 0; x < width; x++) {
190
+ let maxValue = 0;
191
+ for (let ky = 0; ky < 5; ky++) {
192
+ for (let kx = 0; kx < 5; kx++) {
193
+ if (!ALLENK_ELLIPSE_5X5[ky][kx]) continue;
194
+ const sx = x + kx - 2;
195
+ const sy = y + ky - 2;
196
+ if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
197
+ maxValue = Math.max(maxValue, values[sy * width + sx] || 0);
198
+ }
199
+ }
200
+ output[y * width + x] = maxValue;
201
+ }
202
+ }
203
+
204
+ return output;
205
+ }
206
+
207
+ function createAllenkOpenCvGradientMask({ alphaMap, width, height, strength }) {
208
+ if (!alphaMap || width <= 0 || height <= 0 || alphaMap.length < width * height) {
209
+ return new Float32Array(0);
210
+ }
211
+
212
+ const gradient = new Float32Array(width * height);
213
+ let minGradient = Number.POSITIVE_INFINITY;
214
+ let maxGradient = Number.NEGATIVE_INFINITY;
215
+
216
+ for (let y = 0; y < height; y++) {
217
+ for (let x = 0; x < width; x++) {
218
+ const top = reflect101(y - 1, height);
219
+ const bottom = reflect101(y + 1, height);
220
+ const left = reflect101(x - 1, width);
221
+ const right = reflect101(x + 1, width);
222
+ const gx = (
223
+ -alphaMap[top * width + left] -
224
+ 2 * alphaMap[y * width + left] -
225
+ alphaMap[bottom * width + left] +
226
+ alphaMap[top * width + right] +
227
+ 2 * alphaMap[y * width + right] +
228
+ alphaMap[bottom * width + right]
229
+ );
230
+ const gy = (
231
+ -alphaMap[top * width + left] -
232
+ 2 * alphaMap[top * width + x] -
233
+ alphaMap[top * width + right] +
234
+ alphaMap[bottom * width + left] +
235
+ 2 * alphaMap[bottom * width + x] +
236
+ alphaMap[bottom * width + right]
237
+ );
238
+ const value = Math.sqrt(gx * gx + gy * gy);
239
+ gradient[y * width + x] = value;
240
+ minGradient = Math.min(minGradient, value);
241
+ maxGradient = Math.max(maxGradient, value);
242
+ }
243
+ }
244
+
245
+ if (!Number.isFinite(minGradient) || maxGradient <= minGradient) {
246
+ return new Float32Array(width * height);
247
+ }
248
+
249
+ const normalized = new Float32Array(width * height);
250
+ const scale = 1 / (maxGradient - minGradient);
251
+ for (let i = 0; i < normalized.length; i++) {
252
+ normalized[i] = Math.sqrt(clamp((gradient[i] - minGradient) * scale, 0, 1));
253
+ }
254
+
255
+ const expanded = dilateAllenkEllipse5x5(normalized, width, height);
256
+ const blurred = gaussianBlurFloatMap(expanded, width, height, 2);
257
+ const safeStrength = Number.isFinite(strength) ? Math.max(0, strength) : 1;
258
+
259
+ for (let i = 0; i < blurred.length; i++) {
260
+ blurred[i] = clamp(blurred[i] * safeStrength, 0, 1);
261
+ }
262
+
263
+ return blurred;
264
+ }
265
+
126
266
  function normalizeAllenkFdncnnOptions(options = {}) {
127
267
  const sigma = Number.isFinite(options.sigma)
128
268
  ? clamp(options.sigma, 0, ALLENK_FDNCNN_MODEL.maxSigma)
@@ -145,17 +285,14 @@ function createAllenkGradientMask({
145
285
  } = {}) {
146
286
  const sourceSize = inferSquareSize(alphaMap);
147
287
  const resizedAlphaMap = sourceSize > 0
148
- ? resizeSquareAlphaMapArea(alphaMap, sourceSize, width, height)
288
+ ? resizeSquareAlphaMapOpenCv(alphaMap, sourceSize, width, height)
149
289
  : alphaMap;
150
290
 
151
- return createAlphaGradientMask({
291
+ return createAllenkOpenCvGradientMask({
152
292
  alphaMap: resizedAlphaMap,
153
293
  width,
154
294
  height,
155
- strength,
156
- gamma: 0.5,
157
- dilateRadius: 2,
158
- blurSigma: 2
295
+ strength
159
296
  });
160
297
  }
161
298
 
@@ -188,6 +325,58 @@ function calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding = A
188
325
  };
189
326
  }
190
327
 
328
+ function calculateAllenkVirtualPaddedRoi({
329
+ imageWidth,
330
+ imageHeight,
331
+ region,
332
+ padding = ALLENK_FDNCNN_MODEL.defaultPadding,
333
+ targetWidth = null,
334
+ targetHeight = null
335
+ } = {}) {
336
+ if (!region || imageWidth <= 0 || imageHeight <= 0 || region.width <= 0 || region.height <= 0) {
337
+ return null;
338
+ }
339
+
340
+ const safePadding = Math.max(0, Math.round(padding));
341
+ const x = Math.round(region.x - safePadding);
342
+ const y = Math.round(region.y - safePadding);
343
+ const defaultWidth = Math.round(region.width + safePadding * 2);
344
+ const defaultHeight = Math.round(region.height + safePadding * 2);
345
+ const width = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : defaultWidth;
346
+ const height = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : defaultHeight;
347
+
348
+ if (width < 4 || height < 4) return null;
349
+
350
+ const visibleX = clamp(x, 0, imageWidth);
351
+ const visibleY = clamp(y, 0, imageHeight);
352
+ const visibleRight = clamp(x + width, 0, imageWidth);
353
+ const visibleBottom = clamp(y + height, 0, imageHeight);
354
+ const visibleWidth = visibleRight - visibleX;
355
+ const visibleHeight = visibleBottom - visibleY;
356
+ if (visibleWidth <= 0 || visibleHeight <= 0) return null;
357
+
358
+ return {
359
+ x,
360
+ y,
361
+ width,
362
+ height,
363
+ inner: {
364
+ x: clamp(Math.round(region.x - x), 0, width),
365
+ y: clamp(Math.round(region.y - y), 0, height),
366
+ width: clamp(Math.round(region.width), 0, width),
367
+ height: clamp(Math.round(region.height), 0, height)
368
+ },
369
+ visible: {
370
+ x: visibleX,
371
+ y: visibleY,
372
+ width: visibleWidth,
373
+ height: visibleHeight,
374
+ offsetX: visibleX - x,
375
+ offsetY: visibleY - y
376
+ }
377
+ };
378
+ }
379
+
191
380
  function calculateAllenkRuntimeRoi({
192
381
  imageWidth,
193
382
  imageHeight,
@@ -231,6 +420,59 @@ function calculateAllenkRuntimeRoi({
231
420
  };
232
421
  }
233
422
 
423
+ function extractAllenkVirtualImageData({
424
+ imageData,
425
+ roi,
426
+ imageX = 0,
427
+ imageY = 0,
428
+ canvasWidth = imageData?.width || 0,
429
+ canvasHeight = imageData?.height || 0
430
+ } = {}) {
431
+ if (
432
+ !imageData?.data ||
433
+ !roi ||
434
+ imageData.width <= 0 ||
435
+ imageData.height <= 0 ||
436
+ roi.width <= 0 ||
437
+ roi.height <= 0 ||
438
+ canvasWidth <= 0 ||
439
+ canvasHeight <= 0
440
+ ) {
441
+ return { width: 0, height: 0, data: new Uint8ClampedArray(0) };
442
+ }
443
+
444
+ const width = Math.max(1, Math.round(roi.width));
445
+ const height = Math.max(1, Math.round(roi.height));
446
+ const output = new Uint8ClampedArray(width * height * 4);
447
+ const sourceStride = imageData.data.length >= imageData.width * imageData.height * 4 ? 4 : 3;
448
+
449
+ for (let y = 0; y < height; y++) {
450
+ const globalY = clamp(Math.round(roi.y + y), 0, canvasHeight - 1);
451
+ const localY = clamp(globalY - Math.round(imageY), 0, imageData.height - 1);
452
+ for (let x = 0; x < width; x++) {
453
+ const globalX = clamp(Math.round(roi.x + x), 0, canvasWidth - 1);
454
+ const localX = clamp(globalX - Math.round(imageX), 0, imageData.width - 1);
455
+ const src = (localY * imageData.width + localX) * sourceStride;
456
+ const dst = (y * width + x) * 4;
457
+ output[dst] = imageData.data[src] || 0;
458
+ output[dst + 1] = imageData.data[src + 1] || 0;
459
+ output[dst + 2] = imageData.data[src + 2] || 0;
460
+ output[dst + 3] = sourceStride >= 4 ? (imageData.data[src + 3] ?? 255) : 255;
461
+ }
462
+ }
463
+
464
+ const ImageDataCtor = typeof imageData.constructor === 'function' && imageData.constructor !== Object
465
+ ? imageData.constructor
466
+ : typeof ImageData === 'function'
467
+ ? ImageData
468
+ : null;
469
+ if (ImageDataCtor) {
470
+ return new ImageDataCtor(output, width, height);
471
+ }
472
+
473
+ return { width, height, data: output };
474
+ }
475
+
234
476
  function embedAllenkRoiWeights({ roiWeights, roiWidth, roiHeight, paddedRoi, blurSigma = 1 } = {}) {
235
477
  if (!roiWeights || !paddedRoi?.inner || roiWidth <= 0 || roiHeight <= 0) {
236
478
  return new Float32Array(0);
@@ -363,8 +605,10 @@ export {
363
605
  buildAllenkFdncnnInput,
364
606
  calculateAllenkPaddedRoi,
365
607
  calculateAllenkRuntimeRoi,
608
+ calculateAllenkVirtualPaddedRoi,
366
609
  convertAllenkFdncnnOutputToRgba,
367
610
  createAllenkGradientMask,
368
611
  embedAllenkRoiWeights,
612
+ extractAllenkVirtualImageData,
369
613
  normalizeAllenkFdncnnOptions
370
614
  };
@@ -231,12 +231,13 @@ function createGraphProto({
231
231
  segments,
232
232
  inputName,
233
233
  outputName,
234
- roiSize
234
+ roiWidth,
235
+ roiHeight
235
236
  }) {
236
237
  const initializers = createInitializers({ bin, segments });
237
238
  const nodes = createNodes({ segments, inputName, outputName });
238
- const inputShape = [1, segments[0].inputChannels, roiSize, roiSize];
239
- const outputShape = [1, segments[segments.length - 1].outputChannels, roiSize, roiSize];
239
+ const inputShape = [1, segments[0].inputChannels, roiHeight, roiWidth];
240
+ const outputShape = [1, segments[segments.length - 1].outputChannels, roiHeight, roiWidth];
240
241
 
241
242
  return {
242
243
  nodeCount: nodes.length,
@@ -275,6 +276,8 @@ function exportAllenkFdncnnOnnx({
275
276
  bin,
276
277
  weightLayout,
277
278
  roiSize = 72,
279
+ roiWidth = roiSize,
280
+ roiHeight = roiSize,
278
281
  inputName = 'fdncnn_input',
279
282
  outputName = 'fdncnn_output',
280
283
  graphName = 'allenk_fdncnn_color',
@@ -285,8 +288,11 @@ function exportAllenkFdncnnOnnx({
285
288
  if (!segments.length) {
286
289
  throw new Error('allenk FDnCNN ONNX export requires weightLayout.segments');
287
290
  }
288
- if (!Number.isInteger(roiSize) || roiSize <= 0) {
289
- throw new Error(`Invalid ONNX ROI size: ${roiSize}`);
291
+ if (!Number.isInteger(roiWidth) || roiWidth <= 0) {
292
+ throw new Error(`Invalid ONNX ROI width: ${roiWidth}`);
293
+ }
294
+ if (!Number.isInteger(roiHeight) || roiHeight <= 0) {
295
+ throw new Error(`Invalid ONNX ROI height: ${roiHeight}`);
290
296
  }
291
297
 
292
298
  const graph = createGraphProto({
@@ -295,7 +301,8 @@ function exportAllenkFdncnnOnnx({
295
301
  segments,
296
302
  inputName,
297
303
  outputName,
298
- roiSize
304
+ roiWidth,
305
+ roiHeight
299
306
  });
300
307
  const model = createModelProto({
301
308
  graph: graph.bytes,
@@ -313,13 +320,15 @@ function exportAllenkFdncnnOnnx({
313
320
  graphName,
314
321
  inputName,
315
322
  outputName,
316
- inputShape: [1, segments[0].inputChannels, roiSize, roiSize],
317
- outputShape: [1, segments[segments.length - 1].outputChannels, roiSize, roiSize],
323
+ inputShape: [1, segments[0].inputChannels, roiHeight, roiWidth],
324
+ outputShape: [1, segments[segments.length - 1].outputChannels, roiHeight, roiWidth],
318
325
  nodeCount: graph.nodeCount,
319
326
  convolutionNodeCount: segments.length,
320
327
  reluNodeCount: segments.filter((segment, index) => index < segments.length - 1 && segment.activationType === 1).length,
321
328
  initializerCount: graph.initializerCount,
322
- roiSize
329
+ roiSize: roiWidth === roiHeight ? roiWidth : null,
330
+ roiWidth,
331
+ roiHeight
323
332
  }
324
333
  };
325
334
  }
@@ -139,7 +139,7 @@ const PREVIEW_TEMPLATE_ALIGN_SHIFTS = [-1, -0.5, 0, 0.5, 1];
139
139
  const PREVIEW_TEMPLATE_ALIGN_SCALES = [0.985, 1, 1.015];
140
140
  const PREVIEW_ANCHOR_GAIN_SKIP_RESIDUAL_THRESHOLD = 0.22;
141
141
  const PREVIEW_ANCHOR_GAIN_SKIP_GRADIENT_THRESHOLD = 0.24;
142
- const CORE_ALPHA_PRIORITY_GAINS = Object.freeze([0.6, 1, 1.15, 1.3, 0.45, 0.7, 0.85, 0.55]);
142
+ const CORE_ALPHA_PRIORITY_GAINS = Object.freeze([0.6, 1, 1.1, 1.15, 1.3, 0.45, 0.7, 0.85, 0.55]);
143
143
  const CURRENT_LARGE_MARGIN_ULTRA_WEAK_ALPHA_GAINS = Object.freeze([0.25, 0.3, 0.35, 0.4]);
144
144
  const STANDARD_ANCHOR_WEAK_ALPHA_RESCUE_GAINS = Object.freeze([0.55, 0.7, 0.85]);
145
145
  const STANDARD_ANCHOR_WEAK_RESCUE_MAX_SPATIAL = 0.35;
@@ -1044,6 +1044,24 @@ export function pickBetterCandidate(currentBest, candidate, minCostDelta = 0.005
1044
1044
  if (shouldPreserveStrongCanonical96AgainstWeakCurrentLargeMargin(candidate, currentBest)) {
1045
1045
  return candidate;
1046
1046
  }
1047
+ if (shouldPreserveStrongCanonical96AgainstWeakNewMargin(currentBest, candidate)) {
1048
+ return currentBest;
1049
+ }
1050
+ if (shouldPreserveStrongCanonical96AgainstWeakNewMargin(candidate, currentBest)) {
1051
+ return candidate;
1052
+ }
1053
+ if (shouldPreserveStandardAlphaCanonical96AgainstDarkGain(currentBest, candidate)) {
1054
+ return currentBest;
1055
+ }
1056
+ if (shouldPreserveStandardAlphaCanonical96AgainstDarkGain(candidate, currentBest)) {
1057
+ return candidate;
1058
+ }
1059
+ if (shouldPreserveBalancedAlphaCanonical96(currentBest, candidate)) {
1060
+ return currentBest;
1061
+ }
1062
+ if (shouldPreserveBalancedAlphaCanonical96(candidate, currentBest)) {
1063
+ return candidate;
1064
+ }
1047
1065
  if (shouldPreferCatalogOriginalSignal(candidate, currentBest)) {
1048
1066
  return candidate;
1049
1067
  }
@@ -1198,6 +1216,16 @@ function isDefault96GeometryCandidate(candidate) {
1198
1216
  candidate.config.marginBottom === 64;
1199
1217
  }
1200
1218
 
1219
+ function isNewMargin96Candidate(candidate) {
1220
+ return isStandardCandidateSource(candidate) &&
1221
+ candidate?.provenance?.localShift !== true &&
1222
+ candidate?.provenance?.sizeJitter !== true &&
1223
+ candidate?.provenance?.previewAnchor !== true &&
1224
+ candidate?.config?.logoSize === 96 &&
1225
+ candidate.config.marginRight === 192 &&
1226
+ candidate.config.marginBottom === 192;
1227
+ }
1228
+
1201
1229
  function shouldPreserveStrongCanonical96AgainstWeakCurrentLargeMargin(currentBest, candidate) {
1202
1230
  if (!isCanonicalDefault96Candidate(currentBest)) return false;
1203
1231
  if (!isCurrentLargeMarginCatalogCandidate(candidate)) return false;
@@ -1238,6 +1266,111 @@ function shouldPreserveStrongCanonical96AgainstWeakCurrentLargeMargin(currentBes
1238
1266
  candidateHasWeakOriginalSignal;
1239
1267
  }
1240
1268
 
1269
+ function shouldPreserveStrongCanonical96AgainstWeakNewMargin(currentBest, candidate) {
1270
+ if (!isCanonicalDefault96Candidate(currentBest)) return false;
1271
+ if (!isNewMargin96Candidate(candidate)) return false;
1272
+
1273
+ const currentSpatial = Number(currentBest.originalSpatialScore);
1274
+ const currentGradient = Number(currentBest.originalGradientScore);
1275
+ const candidateSpatial = Number(candidate.originalSpatialScore);
1276
+ const candidateGradient = Number(candidate.originalGradientScore);
1277
+ if (
1278
+ !Number.isFinite(currentSpatial) ||
1279
+ !Number.isFinite(currentGradient) ||
1280
+ !Number.isFinite(candidateSpatial) ||
1281
+ !Number.isFinite(candidateGradient)
1282
+ ) {
1283
+ return false;
1284
+ }
1285
+
1286
+ const currentHasStrongCanonicalSignal =
1287
+ currentSpatial >= 0.55 &&
1288
+ currentGradient >= 0.2;
1289
+ const candidateHasStrongNewMarginSignal =
1290
+ candidateSpatial >= 0.55 &&
1291
+ candidateGradient >= 0.3;
1292
+ const candidateHasWeakOrMuchWeakerSignal =
1293
+ candidateSpatial < STANDARD_VALIDATION_MIN_ORIGINAL_SPATIAL_SCORE ||
1294
+ candidateGradient < STANDARD_VALIDATION_MIN_ORIGINAL_GRADIENT_SCORE ||
1295
+ (
1296
+ candidateSpatial <= currentSpatial - STRONG_ORIGINAL_SIGNAL_SPATIAL_ADVANTAGE &&
1297
+ candidateGradient <= currentGradient - STRONG_ORIGINAL_SIGNAL_GRADIENT_ADVANTAGE
1298
+ );
1299
+
1300
+ return currentHasStrongCanonicalSignal &&
1301
+ !candidateHasStrongNewMarginSignal &&
1302
+ candidateHasWeakOrMuchWeakerSignal;
1303
+ }
1304
+
1305
+ function isBalancedCanonical96AlphaGain(candidate) {
1306
+ const alphaGain = Number(candidate?.alphaGain);
1307
+ return Number.isFinite(alphaGain) && alphaGain >= 1 && alphaGain <= 1.1;
1308
+ }
1309
+
1310
+ function shouldPreserveStandardAlphaCanonical96AgainstDarkGain(currentBest, candidate) {
1311
+ if (!sameCandidateAnchor(currentBest, candidate)) return false;
1312
+ if (!isCanonicalDefault96Candidate(currentBest)) return false;
1313
+
1314
+ const currentAlphaGain = Number(currentBest?.alphaGain);
1315
+ const candidateAlphaGain = Number(candidate?.alphaGain);
1316
+ if (
1317
+ !Number.isFinite(currentAlphaGain) ||
1318
+ !Number.isFinite(candidateAlphaGain) ||
1319
+ currentAlphaGain !== 1 ||
1320
+ candidateAlphaGain <= 1 ||
1321
+ candidate?.tooDark !== true
1322
+ ) {
1323
+ return false;
1324
+ }
1325
+
1326
+ const originalSpatial = Number(currentBest.originalSpatialScore);
1327
+ const originalGradient = Number(currentBest.originalGradientScore);
1328
+ const currentSpatial = Number(currentBest.processedSpatialScore);
1329
+ const currentGradient = Number(currentBest.processedGradientScore);
1330
+ if (
1331
+ !Number.isFinite(originalSpatial) ||
1332
+ !Number.isFinite(originalGradient) ||
1333
+ !Number.isFinite(currentSpatial) ||
1334
+ !Number.isFinite(currentGradient)
1335
+ ) {
1336
+ return false;
1337
+ }
1338
+
1339
+ return originalSpatial >= 0.55 &&
1340
+ originalGradient >= 0.2 &&
1341
+ currentSpatial >= 0 &&
1342
+ currentSpatial <= 0.35 &&
1343
+ Math.max(0, currentGradient) <= 0.08;
1344
+ }
1345
+
1346
+ function shouldPreserveBalancedAlphaCanonical96(currentBest, candidate) {
1347
+ if (!sameCandidateAnchor(currentBest, candidate)) return false;
1348
+ if (!isCanonicalDefault96Candidate(currentBest)) return false;
1349
+ if (!isBalancedCanonical96AlphaGain(currentBest)) return false;
1350
+
1351
+ const candidateAlphaGain = Number(candidate?.alphaGain);
1352
+ if (!Number.isFinite(candidateAlphaGain) || candidateAlphaGain <= 1.1) return false;
1353
+
1354
+ const originalSpatial = Number(currentBest.originalSpatialScore);
1355
+ const originalGradient = Number(currentBest.originalGradientScore);
1356
+ const currentSpatial = Number(currentBest.processedSpatialScore);
1357
+ const currentGradient = Number(currentBest.processedGradientScore);
1358
+ if (
1359
+ !Number.isFinite(originalSpatial) ||
1360
+ !Number.isFinite(originalGradient) ||
1361
+ !Number.isFinite(currentSpatial) ||
1362
+ !Number.isFinite(currentGradient)
1363
+ ) {
1364
+ return false;
1365
+ }
1366
+
1367
+ return originalSpatial >= 0.55 &&
1368
+ originalGradient >= 0.2 &&
1369
+ currentSpatial >= 0 &&
1370
+ currentSpatial <= 0.22 &&
1371
+ Math.max(0, currentGradient) <= 0.1;
1372
+ }
1373
+
1241
1374
  function hasWeakDriftEvidence(candidate) {
1242
1375
  const candidateSpatial = Number(candidate?.originalSpatialScore);
1243
1376
  const candidateGradient = Number(candidate?.originalGradientScore);
@@ -22,8 +22,12 @@ import { createSelectionDebugSummary } from './selectionDebug.js';
22
22
  import {
23
23
  calculateWatermarkPosition,
24
24
  detectWatermarkConfig,
25
- resolveInitialStandardConfig
26
- } from './watermarkConfig.js';
25
+ resolveInitialStandardConfig
26
+ } from './watermarkConfig.js';
27
+ import {
28
+ blurAlphaMap,
29
+ buildPreviewNeighborhoodPrior
30
+ } from './previewAlphaCalibration.js';
27
31
 
28
32
  const RESIDUAL_RECALIBRATION_THRESHOLD = 0.5;
29
33
  const MIN_SUPPRESSION_FOR_SKIP_RECALIBRATION = 0.18;
@@ -36,6 +40,7 @@ const SUBPIXEL_REFINE_SCALES = [0.99, 1, 1.01];
36
40
  const ALPHA_PARAMETER_GROUPS = Object.freeze([
37
41
  { name: 'gemini-weak-alpha-202606', alphaGain: 0.6, standardPriority: true },
38
42
  { name: 'gemini-standard-alpha', alphaGain: 1, standardPriority: true },
43
+ { name: 'gemini-balanced-strong-alpha-202606', alphaGain: 1.1 },
39
44
  { name: 'gemini-strong-alpha-202606', alphaGain: 1.15 },
40
45
  { name: 'gemini-strong-alpha-high-202606', alphaGain: 1.3 },
41
46
  { name: 'weak-alpha-extra-conservative', alphaGain: 0.45 },
@@ -136,6 +141,21 @@ const PREVIEW_BACKGROUND_CLEANUP_MIN_RESIDUAL = 0.3;
136
141
  const PREVIEW_BACKGROUND_CLEANUP_MAX_BORDER_STD = 24;
137
142
  const PREVIEW_BACKGROUND_CLEANUP_PAD = 8;
138
143
  const PREVIEW_BACKGROUND_CLEANUP_PRIOR_RADIUS = 10;
144
+ const SMOOTH_PRIOR_LOCATED_MIN_SIZE = 80;
145
+ const SMOOTH_PRIOR_LOCATED_MAX_SIZE = 160;
146
+ const SMOOTH_PRIOR_LOCATED_MIN_BORDER_MEAN = 120;
147
+ const SMOOTH_PRIOR_LOCATED_MIN_SPATIAL = 0.25;
148
+ const SMOOTH_PRIOR_LOCATED_MAX_GRADIENT = 0.22;
149
+ const SMOOTH_PRIOR_LOCATED_MIN_SPATIAL_IMPROVEMENT = 0.16;
150
+ const SMOOTH_PRIOR_LOCATED_MAX_GRADIENT_DRIFT = 0.05;
151
+ const SMOOTH_PRIOR_LOCATED_MIN_ARTIFACT_IMPROVEMENT = 0.025;
152
+ const SMOOTH_PRIOR_LOCATED_MAX_ACCEPTED_GRADIENT = 0.18;
153
+ const SMOOTH_PRIOR_LOCATED_PRESETS = Object.freeze([
154
+ { radius: 24, threshold: 0, blurRadius: 0, strength: 0.75, gamma: 0.45 },
155
+ { radius: 36, threshold: 0, blurRadius: 0, strength: 0.75, gamma: 0.45 },
156
+ { radius: 24, threshold: 0.01, blurRadius: 0, strength: 0.75, gamma: 0.45 },
157
+ { radius: 36, threshold: 0.01, blurRadius: 0, strength: 0.75, gamma: 0.45 }
158
+ ]);
139
159
  const OVER_SUBTRACTION_SPATIAL_THRESHOLD = -0.25;
140
160
  const OVER_SUBTRACTION_GRADIENT_THRESHOLD = 0.35;
141
161
  const OVER_SUBTRACTION_MIN_ABS_SPATIAL_IMPROVEMENT = 0.08;
@@ -1370,7 +1390,7 @@ function expandPosition(position, imageData, pad) {
1370
1390
  };
1371
1391
  }
1372
1392
 
1373
- function measureOuterBorderLuminanceStd(imageData, position, margin = 10) {
1393
+ function measureOuterBorderLuminanceStats(imageData, position, margin = 10) {
1374
1394
  let sum = 0;
1375
1395
  let sq = 0;
1376
1396
  let count = 0;
@@ -1399,16 +1419,219 @@ function measureOuterBorderLuminanceStd(imageData, position, margin = 10) {
1399
1419
  }
1400
1420
  }
1401
1421
 
1402
- if (count <= 0) return Number.POSITIVE_INFINITY;
1422
+ if (count <= 0) {
1423
+ return {
1424
+ mean: Number.POSITIVE_INFINITY,
1425
+ std: Number.POSITIVE_INFINITY,
1426
+ count
1427
+ };
1428
+ }
1403
1429
 
1404
1430
  const mean = sum / count;
1405
- return Math.sqrt(Math.max(0, sq / count - mean * mean));
1431
+ return {
1432
+ mean,
1433
+ std: Math.sqrt(Math.max(0, sq / count - mean * mean)),
1434
+ count
1435
+ };
1436
+ }
1437
+
1438
+ function measureOuterBorderLuminanceStd(imageData, position, margin = 10) {
1439
+ return measureOuterBorderLuminanceStats(imageData, position, margin).std;
1440
+ }
1441
+
1442
+ function clamp01(value) {
1443
+ if (!Number.isFinite(value)) return 0;
1444
+ if (value <= 0) return 0;
1445
+ if (value >= 1) return 1;
1446
+ return value;
1406
1447
  }
1407
1448
 
1408
1449
  function clampChannel(value) {
1409
1450
  return Math.max(0, Math.min(255, Math.round(value)));
1410
1451
  }
1411
1452
 
1453
+ function estimateAlphaMapFromBackgroundPrior({
1454
+ originalImageData,
1455
+ priorImageData,
1456
+ position,
1457
+ threshold,
1458
+ blurRadius
1459
+ }) {
1460
+ const alphaMap = new Float32Array(position.width * position.height);
1461
+
1462
+ for (let row = 0; row < position.height; row++) {
1463
+ for (let col = 0; col < position.width; col++) {
1464
+ const localIndex = row * position.width + col;
1465
+ const pixelIndex = ((position.y + row) * originalImageData.width + position.x + col) * 4;
1466
+ let estimatedAlpha = 0;
1467
+
1468
+ for (let channel = 0; channel < 3; channel++) {
1469
+ const denominator = 255 - priorImageData.data[pixelIndex + channel];
1470
+ if (denominator <= 2) continue;
1471
+
1472
+ estimatedAlpha = Math.max(
1473
+ estimatedAlpha,
1474
+ (originalImageData.data[pixelIndex + channel] - priorImageData.data[pixelIndex + channel]) / denominator
1475
+ );
1476
+ }
1477
+
1478
+ alphaMap[localIndex] = Math.min(0.9, clamp01((estimatedAlpha - threshold) * 1.2));
1479
+ }
1480
+ }
1481
+
1482
+ return blurRadius > 0
1483
+ ? blurAlphaMap(alphaMap, position.width, blurRadius)
1484
+ : alphaMap;
1485
+ }
1486
+
1487
+ function applyEstimatedPriorBlend({
1488
+ sourceImageData,
1489
+ priorImageData,
1490
+ estimatedAlphaMap,
1491
+ position,
1492
+ strength,
1493
+ gamma
1494
+ }) {
1495
+ const candidate = cloneImageData(sourceImageData);
1496
+
1497
+ for (let row = 0; row < position.height; row++) {
1498
+ for (let col = 0; col < position.width; col++) {
1499
+ const localIndex = row * position.width + col;
1500
+ const alpha = estimatedAlphaMap[localIndex];
1501
+ if (alpha <= 0.005) continue;
1502
+
1503
+ const blend = clamp01(Math.pow(alpha, gamma) * strength);
1504
+ if (blend <= 0.005) continue;
1505
+
1506
+ const pixelIndex = ((position.y + row) * sourceImageData.width + position.x + col) * 4;
1507
+ for (let channel = 0; channel < 3; channel++) {
1508
+ candidate.data[pixelIndex + channel] = clampChannel(
1509
+ sourceImageData.data[pixelIndex + channel] * (1 - blend) +
1510
+ priorImageData.data[pixelIndex + channel] * blend
1511
+ );
1512
+ }
1513
+ }
1514
+ }
1515
+
1516
+ return candidate;
1517
+ }
1518
+
1519
+ function refineSmoothLocatedResidualWithEstimatedPrior({
1520
+ originalImageData,
1521
+ currentImageData,
1522
+ alphaMap,
1523
+ position,
1524
+ source,
1525
+ alphaGain,
1526
+ baselineSpatialScore,
1527
+ baselineGradientScore
1528
+ }) {
1529
+ if (
1530
+ typeof source !== 'string' ||
1531
+ !source.includes('located-aggressive') ||
1532
+ position?.width < SMOOTH_PRIOR_LOCATED_MIN_SIZE ||
1533
+ position?.width > SMOOTH_PRIOR_LOCATED_MAX_SIZE ||
1534
+ baselineSpatialScore < SMOOTH_PRIOR_LOCATED_MIN_SPATIAL ||
1535
+ baselineGradientScore > SMOOTH_PRIOR_LOCATED_MAX_GRADIENT
1536
+ ) {
1537
+ return null;
1538
+ }
1539
+
1540
+ const borderStats = measureOuterBorderLuminanceStats(originalImageData, position, 16);
1541
+ if (borderStats.mean < SMOOTH_PRIOR_LOCATED_MIN_BORDER_MEAN) {
1542
+ return null;
1543
+ }
1544
+
1545
+ const baselineArtifacts = assessRemovalDiffArtifacts({
1546
+ originalImageData,
1547
+ candidateImageData: currentImageData,
1548
+ alphaMap,
1549
+ position,
1550
+ alphaGain
1551
+ });
1552
+ const baselineArtifactCost = Number(baselineArtifacts?.visualArtifactCost);
1553
+ if (!Number.isFinite(baselineArtifactCost)) return null;
1554
+
1555
+ let best = null;
1556
+ const priorByRadius = new Map();
1557
+
1558
+ for (const preset of SMOOTH_PRIOR_LOCATED_PRESETS) {
1559
+ let priorImageData = priorByRadius.get(preset.radius);
1560
+ if (!priorImageData) {
1561
+ priorImageData = buildPreviewNeighborhoodPrior({
1562
+ previewImageData: originalImageData,
1563
+ position,
1564
+ radius: preset.radius
1565
+ });
1566
+ priorByRadius.set(preset.radius, priorImageData);
1567
+ }
1568
+
1569
+ const estimatedAlphaMap = estimateAlphaMapFromBackgroundPrior({
1570
+ originalImageData,
1571
+ priorImageData,
1572
+ position,
1573
+ threshold: preset.threshold,
1574
+ blurRadius: preset.blurRadius
1575
+ });
1576
+ const candidateImageData = applyEstimatedPriorBlend({
1577
+ sourceImageData: currentImageData,
1578
+ priorImageData,
1579
+ estimatedAlphaMap,
1580
+ position,
1581
+ strength: preset.strength,
1582
+ gamma: preset.gamma
1583
+ });
1584
+ const spatialScore = computeRegionSpatialCorrelation({
1585
+ imageData: candidateImageData,
1586
+ alphaMap,
1587
+ region: { x: position.x, y: position.y, size: position.width }
1588
+ });
1589
+ const gradientScore = computeRegionGradientCorrelation({
1590
+ imageData: candidateImageData,
1591
+ alphaMap,
1592
+ region: { x: position.x, y: position.y, size: position.width }
1593
+ });
1594
+ const artifacts = assessRemovalDiffArtifacts({
1595
+ originalImageData,
1596
+ candidateImageData,
1597
+ alphaMap,
1598
+ position,
1599
+ alphaGain
1600
+ });
1601
+ const artifactCost = Number(artifacts?.visualArtifactCost);
1602
+ if (!Number.isFinite(artifactCost)) continue;
1603
+
1604
+ const spatialImprovement = Math.abs(baselineSpatialScore) - Math.abs(spatialScore);
1605
+ const gradientDrift = gradientScore - baselineGradientScore;
1606
+ const artifactImprovement = baselineArtifactCost - artifactCost;
1607
+ if (
1608
+ spatialImprovement < SMOOTH_PRIOR_LOCATED_MIN_SPATIAL_IMPROVEMENT ||
1609
+ gradientDrift > SMOOTH_PRIOR_LOCATED_MAX_GRADIENT_DRIFT ||
1610
+ artifactImprovement < SMOOTH_PRIOR_LOCATED_MIN_ARTIFACT_IMPROVEMENT ||
1611
+ gradientScore > SMOOTH_PRIOR_LOCATED_MAX_ACCEPTED_GRADIENT
1612
+ ) {
1613
+ continue;
1614
+ }
1615
+
1616
+ const cost = Math.abs(spatialScore) * 0.75 +
1617
+ Math.max(0, gradientScore) * 0.9 +
1618
+ artifactCost * 0.5;
1619
+ if (!best || cost < best.cost) {
1620
+ best = {
1621
+ imageData: candidateImageData,
1622
+ spatialScore,
1623
+ gradientScore,
1624
+ cost,
1625
+ artifactCost,
1626
+ borderStats,
1627
+ preset
1628
+ };
1629
+ }
1630
+ }
1631
+
1632
+ return best;
1633
+ }
1634
+
1412
1635
  function averageStripColor(imageData, {
1413
1636
  xFrom,
1414
1637
  xTo,
@@ -1873,6 +2096,53 @@ function refineLocatedAggressiveRemoval({
1873
2096
  return best;
1874
2097
  }
1875
2098
 
2099
+ function shouldSkipLocatedAggressiveForCleanCanonical96({
2100
+ config,
2101
+ alphaGain,
2102
+ originalSpatialScore,
2103
+ originalGradientScore,
2104
+ currentSpatialScore,
2105
+ currentGradientScore
2106
+ }) {
2107
+ if (
2108
+ config?.logoSize !== 96 ||
2109
+ config.marginRight !== 64 ||
2110
+ config.marginBottom !== 64
2111
+ ) {
2112
+ return false;
2113
+ }
2114
+
2115
+ const resolvedAlphaGain = Number(alphaGain);
2116
+ const originalSpatial = Number(originalSpatialScore);
2117
+ const originalGradient = Number(originalGradientScore);
2118
+ const currentSpatial = Number(currentSpatialScore);
2119
+ const currentGradient = Number(currentGradientScore);
2120
+ if (
2121
+ !Number.isFinite(resolvedAlphaGain) ||
2122
+ !Number.isFinite(originalSpatial) ||
2123
+ !Number.isFinite(originalGradient) ||
2124
+ !Number.isFinite(currentSpatial) ||
2125
+ !Number.isFinite(currentGradient)
2126
+ ) {
2127
+ return false;
2128
+ }
2129
+
2130
+ const cleanStandardAlpha =
2131
+ resolvedAlphaGain <= 1 &&
2132
+ currentSpatial >= 0 &&
2133
+ currentSpatial <= 0.35 &&
2134
+ Math.max(0, currentGradient) <= 0.08;
2135
+ const cleanBalancedAlpha =
2136
+ resolvedAlphaGain <= 1.1 &&
2137
+ currentSpatial >= 0 &&
2138
+ currentSpatial <= 0.22 &&
2139
+ Math.max(0, currentGradient) <= 0.1;
2140
+
2141
+ return originalSpatial >= 0.55 &&
2142
+ originalGradient >= 0.2 &&
2143
+ (cleanStandardAlpha || cleanBalancedAlpha);
2144
+ }
2145
+
1876
2146
  function shouldRelocateSmallFixedLocalAnchor({
1877
2147
  source,
1878
2148
  config,
@@ -2784,9 +3054,23 @@ export function processWatermarkImageData(imageData, options = {}) {
2784
3054
  }
2785
3055
 
2786
3056
  const locatedAggressiveStartedAt = nowMs();
3057
+ const locatedAggressiveResidualVisibility = assessWatermarkResidualVisibility({
3058
+ imageData: finalImageData,
3059
+ position,
3060
+ alphaMap
3061
+ });
2787
3062
  if (
2788
3063
  options.locatedAggressiveRemoval !== false &&
2789
- smallFixedLocalRelocated?.residualVisibility?.visible !== false
3064
+ smallFixedLocalRelocated?.residualVisibility?.visible !== false &&
3065
+ locatedAggressiveResidualVisibility?.visible !== false &&
3066
+ !shouldSkipLocatedAggressiveForCleanCanonical96({
3067
+ config,
3068
+ alphaGain,
3069
+ originalSpatialScore,
3070
+ originalGradientScore,
3071
+ currentSpatialScore: finalProcessedSpatialScore,
3072
+ currentGradientScore: finalProcessedGradientScore
3073
+ })
2790
3074
  ) {
2791
3075
  const aggressiveRefined = refineLocatedAggressiveRemoval({
2792
3076
  originalImageData,
@@ -2835,10 +3119,42 @@ export function processWatermarkImageData(imageData, options = {}) {
2835
3119
  : `${source}+located-aggressive`;
2836
3120
  }
2837
3121
  }
3122
+
3123
+ const smoothPriorStartedAt = nowMs();
3124
+ const smoothPriorRefined = refineSmoothLocatedResidualWithEstimatedPrior({
3125
+ originalImageData,
3126
+ currentImageData: finalImageData,
3127
+ alphaMap,
3128
+ position,
3129
+ source,
3130
+ alphaGain,
3131
+ baselineSpatialScore: finalProcessedSpatialScore,
3132
+ baselineGradientScore: finalProcessedGradientScore
3133
+ });
3134
+ if (smoothPriorRefined) {
3135
+ recordAlphaAdjustmentStage({
3136
+ stage: 'smooth-located-estimated-prior',
3137
+ fromAlphaGain: alphaGain,
3138
+ toAlphaGain: alphaGain,
3139
+ beforeSpatialScore: finalProcessedSpatialScore,
3140
+ beforeGradientScore: finalProcessedGradientScore,
3141
+ afterSpatialScore: smoothPriorRefined.spatialScore,
3142
+ afterGradientScore: smoothPriorRefined.gradientScore,
3143
+ suppressionGain: originalSpatialScore - smoothPriorRefined.spatialScore,
3144
+ cost: smoothPriorRefined.cost,
3145
+ allowSameAlphaGain: true
3146
+ });
3147
+ finalImageData = smoothPriorRefined.imageData;
3148
+ finalProcessedSpatialScore = smoothPriorRefined.spatialScore;
3149
+ finalProcessedGradientScore = smoothPriorRefined.gradientScore;
3150
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
3151
+ source = `${source}+smooth-prior`;
3152
+ }
2838
3153
  if (debugTimingsEnabled) {
2839
3154
  debugTimings.previewEdgeCleanupMs = previewEdgeCleanupElapsedMs;
2840
3155
  debugTimings.smallPreviewRefinementMs = nowMs() - smallPreviewRefinementStartedAt;
2841
3156
  debugTimings.locatedAggressiveRemovalMs = nowMs() - locatedAggressiveStartedAt;
3157
+ debugTimings.smoothPriorCleanupMs = nowMs() - smoothPriorStartedAt;
2842
3158
  debugTimings.totalMs = nowMs() - totalStartedAt;
2843
3159
  }
2844
3160