@pilio/gemini-watermark-remover 1.0.19 → 1.0.21

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.
@@ -1,9 +1,13 @@
1
1
  import { removeWatermark } from './blendModes.js';
2
2
  import {
3
- computeRegionGradientCorrelation,
4
- computeRegionSpatialCorrelation,
5
- warpAlphaMap
6
- } from './adaptiveDetector.js';
3
+ computeRegionGradientCorrelation,
4
+ computeRegionSpatialCorrelation,
5
+ warpAlphaMap
6
+ } from './adaptiveDetector.js';
7
+ import {
8
+ createAlphaGradientMask,
9
+ getAlphaGradientWeight
10
+ } from './alphaGradientMask.js';
7
11
  import {
8
12
  calculateNearBlackRatio,
9
13
  scoreRegion,
@@ -11,7 +15,8 @@ import {
11
15
  } from './candidateSelector.js';
12
16
  import {
13
17
  assessAlphaBandHalo,
14
- assessRemovalDiffArtifacts
18
+ assessRemovalDiffArtifacts,
19
+ assessWatermarkResidualVisibility
15
20
  } from './restorationMetrics.js';
16
21
  import { createSelectionDebugSummary } from './selectionDebug.js';
17
22
  import {
@@ -39,13 +44,57 @@ const ALPHA_PARAMETER_GROUPS = Object.freeze([
39
44
  { name: 'weak-alpha-conservative', alphaGain: 0.55 }
40
45
  ]);
41
46
  const ALPHA_GAIN_CANDIDATES = ALPHA_PARAMETER_GROUPS.map((group) => group.alphaGain);
47
+ const LOCATED_AGGRESSIVE_ALPHA_GAINS = Object.freeze([0.85, 1, 1.15, 1.3, 1.45, 1.7, 2, 2.4]);
42
48
  const ENABLE_VISUAL_POST_PROCESSING = false;
43
49
  const CATALOG_DARK_ALPHA_GAIN_CANDIDATES = Object.freeze([0.9, 0.85, 0.8, 0.95, 0.7, 0.6]);
44
50
  const STANDARD_ALPHA_PRIORITY_GAINS = ALPHA_PARAMETER_GROUPS
45
51
  .filter((group) => group.standardPriority === true)
46
52
  .map((group) => group.alphaGain);
47
53
  const PREVIEW_EDGE_CLEANUP_MAX_SIZE = 40;
48
- const PREVIEW_EDGE_CLEANUP_SPATIAL_THRESHOLD = 0.08;
54
+ const KNOWN_48_EDGE_CLEANUP_MIN_SIZE = 40;
55
+ const KNOWN_48_EDGE_CLEANUP_MAX_SIZE = 56;
56
+ const KNOWN_48_EDGE_CLEANUP_MIN_GRADIENT = 0.22;
57
+ const KNOWN_48_EDGE_CLEANUP_MAX_ABS_SPATIAL = 0.55;
58
+ const KNOWN_48_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT = 0.015;
59
+ const KNOWN_48_EDGE_CLEANUP_MAX_SPATIAL_DRIFT = 0.06;
60
+ const V2_SMALL_EDGE_CLEANUP_SIZE = 36;
61
+ const V2_SMALL_EDGE_CLEANUP_SIZE_TOLERANCE = 2;
62
+ const V2_SMALL_EDGE_CLEANUP_MIN_GRADIENT = 0.22;
63
+ const V2_SMALL_EDGE_CLEANUP_MAX_ABS_SPATIAL = 0.08;
64
+ const V2_SMALL_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT = 0.025;
65
+ const V2_SMALL_EDGE_CLEANUP_MAX_SPATIAL_DRIFT = 0.035;
66
+ const KNOWN_48_FLAT_FILL_MIN_GRADIENT = 0.28;
67
+ const KNOWN_48_FLAT_FILL_MAX_BACKGROUND_STD = 6;
68
+ const KNOWN_48_FLAT_FILL_MIN_GRADIENT_IMPROVEMENT = 0.045;
69
+ const KNOWN_48_FLAT_FILL_SECOND_PASS_MIN_GRADIENT_IMPROVEMENT = 0.025;
70
+ const KNOWN_48_FLAT_FILL_MAX_APPLIED_PASSES = 2;
71
+ const KNOWN_48_FLAT_FILL_MAX_SPATIAL_DRIFT = 0.12;
72
+ const KNOWN_48_FLAT_FILL_MAX_ACCEPTED_ABS_SPATIAL = 0.38;
73
+ const KNOWN_48_FLAT_FILL_PAD = 10;
74
+ const KNOWN_48_FLAT_FILL_OUTSIDE_ALPHA_MAX = 0.012;
75
+ const KNOWN_48_FLAT_FILL_PRESETS = Object.freeze([
76
+ { name: 'edge', minAlpha: 0.012, maxAlpha: 0.5, strength: 0.9 },
77
+ { name: 'wide', minAlpha: 0.008, maxAlpha: 0.99, strength: 0.92 },
78
+ { name: 'hard', minAlpha: 0.004, maxAlpha: 0.99, strength: 1 }
79
+ ]);
80
+ const NEW_MARGIN_96_FLAT_FILL_MIN_GRADIENT = 0.12;
81
+ const NEW_MARGIN_96_FLAT_FILL_MAX_BACKGROUND_STD = 7;
82
+ const NEW_MARGIN_96_FLAT_FILL_MIN_GRADIENT_IMPROVEMENT = 0.025;
83
+ const NEW_MARGIN_96_FLAT_FILL_MAX_SPATIAL_DRIFT = 0.08;
84
+ const NEW_MARGIN_96_FLAT_FILL_MAX_ACCEPTED_ABS_SPATIAL = 0.22;
85
+ const NEW_MARGIN_96_FLAT_FILL_PRESETS = Object.freeze([
86
+ { name: 'edge', minAlpha: 0.006, maxAlpha: 0.45, strength: 0.75 }
87
+ ]);
88
+ const KNOWN_48_LUMA_EDGE_MIN_GRADIENT = 0.2;
89
+ const KNOWN_48_LUMA_EDGE_MIN_GRADIENT_IMPROVEMENT = 0.02;
90
+ const KNOWN_48_LUMA_EDGE_MAX_SPATIAL_DRIFT = 0.04;
91
+ const KNOWN_48_LUMA_EDGE_MAX_ACCEPTED_ABS_SPATIAL = 0.38;
92
+ const KNOWN_48_LUMA_EDGE_PRESETS = Object.freeze([
93
+ { name: 'soft', minAlpha: 0.012, maxAlpha: 0.7, referenceAlphaMax: 0.025, radius: 2, strength: 0.28, colorSigma: 34, maxDelta: 22 },
94
+ { name: 'mid', minAlpha: 0.012, maxAlpha: 0.7, referenceAlphaMax: 0.04, radius: 3, strength: 0.42, colorSigma: 34, maxDelta: 32 },
95
+ { name: 'wide', minAlpha: 0.012, maxAlpha: 0.7, referenceAlphaMax: 0.055, radius: 4, strength: 0.48, colorSigma: 34, maxDelta: 40 }
96
+ ]);
97
+ const PREVIEW_EDGE_CLEANUP_SPATIAL_THRESHOLD = 0.08;
49
98
  const PREVIEW_EDGE_CLEANUP_GRADIENT_THRESHOLD = 0.1;
50
99
  const PREVIEW_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT = 0.03;
51
100
  const PREVIEW_EDGE_CLEANUP_MAX_SPATIAL_DRIFT = 0.04;
@@ -76,6 +125,12 @@ const PREVIEW_EDGE_CLEANUP_AGGRESSIVE_PRESETS = Object.freeze([
76
125
  maxAcceptedSpatial: 0.18
77
126
  }
78
127
  ]);
128
+ const LOCATED_AGGRESSIVE_EDGE_PRESETS = Object.freeze([
129
+ { minAlpha: 0.004, maxAlpha: 0.99, radius: 2, strength: 0.85, outsideAlphaMax: 0.08 },
130
+ { minAlpha: 0.004, maxAlpha: 0.99, radius: 3, strength: 1.15, outsideAlphaMax: 0.12 },
131
+ { minAlpha: 0.004, maxAlpha: 0.99, radius: 5, strength: 1.45, outsideAlphaMax: 0.18 },
132
+ { minAlpha: 0.02, maxAlpha: 0.99, radius: 6, strength: 1.8, outsideAlphaMax: 0.25 }
133
+ ]);
79
134
  const PREVIEW_BACKGROUND_CLEANUP_MAX_SIZE = 52;
80
135
  const PREVIEW_BACKGROUND_CLEANUP_MIN_RESIDUAL = 0.3;
81
136
  const PREVIEW_BACKGROUND_CLEANUP_MAX_BORDER_STD = 24;
@@ -91,6 +146,10 @@ const WEAK_ALPHA_FINE_TUNE_MIN_ORIGINAL_SPATIAL = 0.45;
91
146
  const WEAK_ALPHA_FINE_TUNE_MIN_POSITIVE_RESIDUAL = 0.05;
92
147
  const WEAK_ALPHA_FINE_TUNE_MIN_ABS_SPATIAL_IMPROVEMENT = 0.04;
93
148
  const WEAK_ALPHA_FINE_TUNE_MAX_GRADIENT_INCREASE = 0.08;
149
+ const STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_SPATIAL = 0.75;
150
+ const STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_GRADIENT = 0.65;
151
+ const STRONG_POSITIVE_FINE_TUNE_MIN_CURRENT_SPATIAL = 0.3;
152
+ const STRONG_POSITIVE_FINE_TUNE_EXTRA_GAINS = Object.freeze([0.95, 1]);
94
153
  const CATALOG_ALPHA_DARK_FINE_TUNE_MIN_ORIGINAL_SPATIAL = 0.6;
95
154
  const CATALOG_ALPHA_DARK_FINE_TUNE_MIN_ORIGINAL_GRADIENT = 0.45;
96
155
  const CATALOG_ALPHA_DARK_FINE_TUNE_MAX_NEGATIVE_RESIDUAL = -0.12;
@@ -143,13 +202,20 @@ function normalizeMetaPosition(position) {
143
202
  function normalizeMetaConfig(config) {
144
203
  if (!config) return null;
145
204
 
146
- const { logoSize, marginRight, marginBottom } = config;
147
- if (![logoSize, marginRight, marginBottom].every((value) => Number.isFinite(value))) {
148
- return null;
149
- }
150
-
151
- return { logoSize, marginRight, marginBottom };
152
- }
205
+ const { logoSize, marginRight, marginBottom } = config;
206
+ if (![logoSize, marginRight, marginBottom].every((value) => Number.isFinite(value))) {
207
+ return null;
208
+ }
209
+
210
+ return {
211
+ logoSize,
212
+ marginRight,
213
+ marginBottom,
214
+ ...(typeof config.alphaVariant === 'string' && config.alphaVariant.length > 0
215
+ ? { alphaVariant: config.alphaVariant }
216
+ : {})
217
+ };
218
+ }
153
219
 
154
220
  function createWatermarkMeta({
155
221
  position = null,
@@ -157,10 +223,11 @@ function createWatermarkMeta({
157
223
  adaptiveConfidence = null,
158
224
  originalSpatialScore = null,
159
225
  originalGradientScore = null,
160
- processedSpatialScore = null,
161
- processedGradientScore = null,
162
- suppressionGain = null,
163
- templateWarp = null,
226
+ processedSpatialScore = null,
227
+ processedGradientScore = null,
228
+ suppressionGain = null,
229
+ residualVisibility = null,
230
+ templateWarp = null,
164
231
  alphaGain = 1,
165
232
  passCount = 0,
166
233
  attemptedPassCount = 0,
@@ -185,11 +252,12 @@ function createWatermarkMeta({
185
252
  detection: {
186
253
  adaptiveConfidence,
187
254
  originalSpatialScore,
188
- originalGradientScore,
189
- processedSpatialScore,
190
- processedGradientScore,
191
- suppressionGain
192
- },
255
+ originalGradientScore,
256
+ processedSpatialScore,
257
+ processedGradientScore,
258
+ suppressionGain,
259
+ residualVisibility
260
+ },
193
261
  templateWarp: templateWarp ?? null,
194
262
  alphaGain,
195
263
  passCount,
@@ -470,6 +538,7 @@ function fineTuneWeakPositiveResidualAlpha({
470
538
  currentGradientScore,
471
539
  currentAlphaGain,
472
540
  originalSpatialScore,
541
+ originalGradientScore,
473
542
  originalNearBlackRatio
474
543
  }) {
475
544
  if (
@@ -483,10 +552,26 @@ function fineTuneWeakPositiveResidualAlpha({
483
552
  const maxAllowedNearBlackRatio = Math.min(1, originalNearBlackRatio + MAX_NEAR_BLACK_RATIO_INCREASE);
484
553
  let best = null;
485
554
  const fineStepCount = Math.round(OVER_SUBTRACTION_FINE_ALPHA_WINDOW / OVER_SUBTRACTION_FINE_ALPHA_STEP);
555
+ const alphaGainCandidates = new Set();
486
556
 
487
557
  for (let step = 1; step <= fineStepCount; step++) {
488
558
  const alphaGain = Number((currentAlphaGain + step * OVER_SUBTRACTION_FINE_ALPHA_STEP).toFixed(2));
489
559
  if (alphaGain >= 1) continue;
560
+ alphaGainCandidates.add(alphaGain);
561
+ }
562
+
563
+ if (
564
+ currentSpatialScore >= STRONG_POSITIVE_FINE_TUNE_MIN_CURRENT_SPATIAL &&
565
+ originalSpatialScore >= STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_SPATIAL &&
566
+ originalGradientScore >= STRONG_POSITIVE_FINE_TUNE_MIN_ORIGINAL_GRADIENT
567
+ ) {
568
+ for (const alphaGain of STRONG_POSITIVE_FINE_TUNE_EXTRA_GAINS) {
569
+ alphaGainCandidates.add(alphaGain);
570
+ }
571
+ }
572
+
573
+ for (const alphaGain of alphaGainCandidates) {
574
+ if (alphaGain <= currentAlphaGain || alphaGain > 1) continue;
490
575
 
491
576
  const candidate = cloneImageData(originalImageData);
492
577
  removeWatermark(candidate, alphaMap, position, { alphaGain });
@@ -619,13 +704,31 @@ function fineTuneDarkCatalogAlpha({
619
704
  return best;
620
705
  }
621
706
 
622
- function shouldRefinePreviewResidualEdge({
707
+ function shouldRefineResidualEdge({
623
708
  source,
624
709
  position,
625
710
  baselineSpatialScore,
626
711
  baselineGradientScore,
627
- baselinePositiveHalo
712
+ baselinePositiveHalo,
713
+ mode = 'preview'
628
714
  }) {
715
+ if (mode === 'known-48') {
716
+ return position?.width >= KNOWN_48_EDGE_CLEANUP_MIN_SIZE &&
717
+ position?.width <= KNOWN_48_EDGE_CLEANUP_MAX_SIZE &&
718
+ Math.abs(baselineSpatialScore) <= KNOWN_48_EDGE_CLEANUP_MAX_ABS_SPATIAL &&
719
+ (
720
+ baselineGradientScore >= KNOWN_48_EDGE_CLEANUP_MIN_GRADIENT ||
721
+ baselinePositiveHalo >= PREVIEW_EDGE_CLEANUP_STRONG_HALO_THRESHOLD
722
+ );
723
+ }
724
+
725
+ if (mode === 'v2-small') {
726
+ return position?.width >= V2_SMALL_EDGE_CLEANUP_SIZE - V2_SMALL_EDGE_CLEANUP_SIZE_TOLERANCE &&
727
+ position?.width <= V2_SMALL_EDGE_CLEANUP_SIZE + V2_SMALL_EDGE_CLEANUP_SIZE_TOLERANCE &&
728
+ Math.abs(baselineSpatialScore) <= V2_SMALL_EDGE_CLEANUP_MAX_ABS_SPATIAL &&
729
+ baselineGradientScore >= V2_SMALL_EDGE_CLEANUP_MIN_GRADIENT;
730
+ }
731
+
629
732
  return typeof source === 'string' &&
630
733
  source.includes('preview-anchor') &&
631
734
  position?.width >= 24 &&
@@ -645,11 +748,65 @@ function shouldUsePreviewAnchorFastCleanup(selectedTrial, position) {
645
748
  position?.width >= 24 &&
646
749
  position?.width <= PREVIEW_EDGE_CLEANUP_MAX_SIZE;
647
750
  }
648
-
751
+
752
+ function isKnown48AnchorConfig(config) {
753
+ if (!config || config.logoSize < KNOWN_48_EDGE_CLEANUP_MIN_SIZE || config.logoSize > KNOWN_48_EDGE_CLEANUP_MAX_SIZE) {
754
+ return false;
755
+ }
756
+
757
+ const marginRight = Number(config.marginRight);
758
+ const marginBottom = Number(config.marginBottom);
759
+ if (!Number.isFinite(marginRight) || !Number.isFinite(marginBottom)) return false;
760
+
761
+ const isCurrentLargeMargin = Math.abs(marginRight - 96) <= 2 && Math.abs(marginBottom - 96) <= 2;
762
+ const isCurrentStandardMargin = marginRight >= 28 && marginRight <= 36 && marginBottom >= 28 && marginBottom <= 36;
763
+ return isCurrentLargeMargin || isCurrentStandardMargin;
764
+ }
765
+
766
+ function shouldUseKnown48EdgeCleanup({ selectedTrial, position, source }) {
767
+ if (selectedTrial?.provenance?.previewAnchor === true) return false;
768
+ if (position?.width < KNOWN_48_EDGE_CLEANUP_MIN_SIZE || position?.width > KNOWN_48_EDGE_CLEANUP_MAX_SIZE) return false;
769
+ if (!isKnown48AnchorConfig(selectedTrial?.config)) return false;
770
+
771
+ const sourceText = String(source || '');
772
+ return sourceText === 'standard' ||
773
+ sourceText.startsWith('standard+gain') ||
774
+ sourceText.includes('catalog') ||
775
+ sourceText.includes('fixed-local');
776
+ }
777
+
778
+ function isV2SmallAnchorConfig(config) {
779
+ if (!config || config.logoSize !== V2_SMALL_EDGE_CLEANUP_SIZE || config.alphaVariant !== 'v2') {
780
+ return false;
781
+ }
782
+
783
+ const marginRight = Number(config.marginRight);
784
+ const marginBottom = Number(config.marginBottom);
785
+ return Number.isFinite(marginRight) &&
786
+ Number.isFinite(marginBottom) &&
787
+ marginRight >= 48 &&
788
+ marginBottom >= 48;
789
+ }
790
+
791
+ function shouldUseV2SmallEdgeCleanup({ selectedTrial, position, source }) {
792
+ if (selectedTrial?.provenance?.previewAnchor === true) return false;
793
+ if (
794
+ position?.width < V2_SMALL_EDGE_CLEANUP_SIZE - V2_SMALL_EDGE_CLEANUP_SIZE_TOLERANCE ||
795
+ position?.width > V2_SMALL_EDGE_CLEANUP_SIZE + V2_SMALL_EDGE_CLEANUP_SIZE_TOLERANCE
796
+ ) {
797
+ return false;
798
+ }
799
+ if (!isV2SmallAnchorConfig(selectedTrial?.config)) return false;
800
+ if (selectedTrial?.provenance?.catalogFamily !== 'gemini-v2-small') return false;
801
+
802
+ const sourceText = String(source || '');
803
+ return sourceText.includes('catalog');
804
+ }
805
+
649
806
  function blendPreviewResidualEdge({
650
- sourceImageData,
651
- alphaMap,
652
- position,
807
+ sourceImageData,
808
+ alphaMap,
809
+ position,
653
810
  minAlpha,
654
811
  maxAlpha,
655
812
  radius,
@@ -657,14 +814,20 @@ function blendPreviewResidualEdge({
657
814
  outsideAlphaMax
658
815
  }) {
659
816
  const candidate = cloneImageData(sourceImageData);
660
- const { width: imageWidth, height: imageHeight, data } = sourceImageData;
661
- const regionSize = position.width;
662
- const maxAlphaSafe = Math.max(maxAlpha, 1e-6);
663
-
664
- for (let row = 0; row < regionSize; row++) {
665
- for (let col = 0; col < regionSize; col++) {
666
- const alpha = alphaMap[row * regionSize + col];
667
- if (alpha < minAlpha || alpha > maxAlpha) continue;
817
+ const { width: imageWidth, height: imageHeight, data } = sourceImageData;
818
+ const regionSize = position.width;
819
+ const maxAlphaSafe = Math.max(maxAlpha, 1e-6);
820
+ const edgeMask = createAlphaGradientMask({
821
+ alphaMap,
822
+ width: regionSize,
823
+ height: regionSize
824
+ });
825
+
826
+ for (let row = 0; row < regionSize; row++) {
827
+ for (let col = 0; col < regionSize; col++) {
828
+ const localIndex = row * regionSize + col;
829
+ const alpha = Math.abs(alphaMap[localIndex]);
830
+ if (alpha < minAlpha || alpha > maxAlpha) continue;
668
831
 
669
832
  let sumR = 0;
670
833
  let sumG = 0;
@@ -684,10 +847,10 @@ function blendPreviewResidualEdge({
684
847
  continue;
685
848
  }
686
849
 
687
- let neighborAlpha = 0;
688
- if (localY >= 0 && localX >= 0 && localY < regionSize && localX < regionSize) {
689
- neighborAlpha = alphaMap[localY * regionSize + localX];
690
- }
850
+ let neighborAlpha = 0;
851
+ if (localY >= 0 && localX >= 0 && localY < regionSize && localX < regionSize) {
852
+ neighborAlpha = Math.abs(alphaMap[localY * regionSize + localX]);
853
+ }
691
854
  if (neighborAlpha > outsideAlphaMax) continue;
692
855
 
693
856
  const distance = Math.sqrt(dx * dx + dy * dy) || 1;
@@ -702,15 +865,472 @@ function blendPreviewResidualEdge({
702
865
 
703
866
  if (sumWeight <= 0) continue;
704
867
 
705
- const blend = Math.max(0, Math.min(1, strength * alpha / maxAlphaSafe));
706
- const pixelIndex = ((position.y + row) * imageWidth + (position.x + col)) * 4;
868
+ const edgeWeight = getAlphaGradientWeight(edgeMask, localIndex);
869
+ const blend = Math.max(0, Math.min(1, strength * alpha / maxAlphaSafe * edgeWeight));
870
+ const pixelIndex = ((position.y + row) * imageWidth + (position.x + col)) * 4;
707
871
  candidate.data[pixelIndex] = Math.round(data[pixelIndex] * (1 - blend) + (sumR / sumWeight) * blend);
708
872
  candidate.data[pixelIndex + 1] = Math.round(data[pixelIndex + 1] * (1 - blend) + (sumG / sumWeight) * blend);
709
873
  candidate.data[pixelIndex + 2] = Math.round(data[pixelIndex + 2] * (1 - blend) + (sumB / sumWeight) * blend);
710
874
  }
711
875
  }
712
-
713
- return candidate;
876
+
877
+ return candidate;
878
+ }
879
+
880
+ function solveLinear3x3(matrix, vector) {
881
+ const augmented = matrix.map((row, index) => [...row, vector[index]]);
882
+
883
+ for (let column = 0; column < 3; column++) {
884
+ let pivot = column;
885
+ for (let row = column + 1; row < 3; row++) {
886
+ if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) {
887
+ pivot = row;
888
+ }
889
+ }
890
+
891
+ if (Math.abs(augmented[pivot][column]) < 1e-8) return null;
892
+ if (pivot !== column) {
893
+ [augmented[pivot], augmented[column]] = [augmented[column], augmented[pivot]];
894
+ }
895
+
896
+ const divisor = augmented[column][column];
897
+ for (let next = column; next < 4; next++) {
898
+ augmented[column][next] /= divisor;
899
+ }
900
+
901
+ for (let row = 0; row < 3; row++) {
902
+ if (row === column) continue;
903
+ const factor = augmented[row][column];
904
+ for (let next = column; next < 4; next++) {
905
+ augmented[row][next] -= factor * augmented[column][next];
906
+ }
907
+ }
908
+ }
909
+
910
+ return [augmented[0][3], augmented[1][3], augmented[2][3]];
911
+ }
912
+
913
+ function fitColorPlane(samples, channel) {
914
+ const matrix = [
915
+ [0, 0, 0],
916
+ [0, 0, 0],
917
+ [0, 0, 0]
918
+ ];
919
+ const vector = [0, 0, 0];
920
+
921
+ for (const sample of samples) {
922
+ const terms = [1, sample.x, sample.y];
923
+ const value = sample[channel];
924
+ for (let row = 0; row < 3; row++) {
925
+ vector[row] += terms[row] * value;
926
+ for (let column = 0; column < 3; column++) {
927
+ matrix[row][column] += terms[row] * terms[column];
928
+ }
929
+ }
930
+ }
931
+
932
+ return solveLinear3x3(matrix, vector);
933
+ }
934
+
935
+ function calculateBackgroundSampleStats(samples) {
936
+ let sum = 0;
937
+ let squareSum = 0;
938
+
939
+ for (const sample of samples) {
940
+ const luminance = 0.2126 * sample.r + 0.7152 * sample.g + 0.0722 * sample.b;
941
+ sum += luminance;
942
+ squareSum += luminance * luminance;
943
+ }
944
+
945
+ const count = samples.length;
946
+ const mean = count > 0 ? sum / count : 0;
947
+ return {
948
+ count,
949
+ mean,
950
+ std: count > 0 ? Math.sqrt(Math.max(0, squareSum / count - mean * mean)) : Number.POSITIVE_INFINITY
951
+ };
952
+ }
953
+
954
+ function sampleFlatBackgroundPixels(imageData, alphaMap, position, {
955
+ pad = KNOWN_48_FLAT_FILL_PAD,
956
+ outsideAlphaMax = KNOWN_48_FLAT_FILL_OUTSIDE_ALPHA_MAX
957
+ } = {}) {
958
+ const samples = [];
959
+ const left = Math.max(0, position.x - pad);
960
+ const top = Math.max(0, position.y - pad);
961
+ const right = Math.min(imageData.width - 1, position.x + position.width + pad - 1);
962
+ const bottom = Math.min(imageData.height - 1, position.y + position.height + pad - 1);
963
+
964
+ for (let y = top; y <= bottom; y++) {
965
+ for (let x = left; x <= right; x++) {
966
+ const inside = x >= position.x &&
967
+ x < position.x + position.width &&
968
+ y >= position.y &&
969
+ y < position.y + position.height;
970
+
971
+ if (inside) {
972
+ const row = y - position.y;
973
+ const col = x - position.x;
974
+ if (alphaMap[row * position.width + col] > outsideAlphaMax) {
975
+ continue;
976
+ }
977
+ }
978
+
979
+ const pixelIndex = (y * imageData.width + x) * 4;
980
+ samples.push({
981
+ x: (x - position.x) / Math.max(1, position.width),
982
+ y: (y - position.y) / Math.max(1, position.height),
983
+ r: imageData.data[pixelIndex],
984
+ g: imageData.data[pixelIndex + 1],
985
+ b: imageData.data[pixelIndex + 2]
986
+ });
987
+ }
988
+ }
989
+
990
+ return samples;
991
+ }
992
+
993
+ function applyFlatBackgroundFill({
994
+ sourceImageData,
995
+ alphaMap,
996
+ position,
997
+ minAlpha,
998
+ maxAlpha,
999
+ strength,
1000
+ maxBackgroundStd = KNOWN_48_FLAT_FILL_MAX_BACKGROUND_STD,
1001
+ edgeWeightFloor = 0.35
1002
+ }) {
1003
+ const samples = sampleFlatBackgroundPixels(sourceImageData, alphaMap, position);
1004
+ const stats = calculateBackgroundSampleStats(samples);
1005
+ if (
1006
+ stats.count < 24 ||
1007
+ stats.std > maxBackgroundStd
1008
+ ) {
1009
+ return null;
1010
+ }
1011
+
1012
+ const redPlane = fitColorPlane(samples, 'r');
1013
+ const greenPlane = fitColorPlane(samples, 'g');
1014
+ const bluePlane = fitColorPlane(samples, 'b');
1015
+ if (!redPlane || !greenPlane || !bluePlane) return null;
1016
+
1017
+ const candidate = cloneImageData(sourceImageData);
1018
+ const edgeMask = createAlphaGradientMask({
1019
+ alphaMap,
1020
+ width: position.width,
1021
+ height: position.height
1022
+ });
1023
+ for (let row = 0; row < position.height; row++) {
1024
+ for (let col = 0; col < position.width; col++) {
1025
+ const localIndex = row * position.width + col;
1026
+ const alpha = alphaMap[localIndex];
1027
+ if (alpha < minAlpha || alpha > maxAlpha) continue;
1028
+
1029
+ const x = col / Math.max(1, position.width);
1030
+ const y = row / Math.max(1, position.height);
1031
+ const target = [
1032
+ redPlane[0] + redPlane[1] * x + redPlane[2] * y,
1033
+ greenPlane[0] + greenPlane[1] * x + greenPlane[2] * y,
1034
+ bluePlane[0] + bluePlane[1] * x + bluePlane[2] * y
1035
+ ];
1036
+ const pixelIndex = ((position.y + row) * candidate.width + position.x + col) * 4;
1037
+ const edgeWeight = getAlphaGradientWeight(edgeMask, localIndex, edgeWeightFloor);
1038
+ const blend = Math.max(0, Math.min(1, strength * Math.min(1, alpha / 0.2) * edgeWeight));
1039
+ for (let channel = 0; channel < 3; channel++) {
1040
+ candidate.data[pixelIndex + channel] = clampChannel(
1041
+ candidate.data[pixelIndex + channel] * (1 - blend) + target[channel] * blend
1042
+ );
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ return { imageData: candidate, stats };
1048
+ }
1049
+
1050
+ function refineKnown48FlatBackgroundResidual({
1051
+ sourceImageData,
1052
+ alphaMap,
1053
+ position,
1054
+ baselineSpatialScore,
1055
+ baselineGradientScore,
1056
+ minGradientImprovement = KNOWN_48_FLAT_FILL_MIN_GRADIENT_IMPROVEMENT
1057
+ }) {
1058
+ if (
1059
+ position?.width < KNOWN_48_EDGE_CLEANUP_MIN_SIZE ||
1060
+ position?.width > KNOWN_48_EDGE_CLEANUP_MAX_SIZE ||
1061
+ baselineGradientScore < KNOWN_48_FLAT_FILL_MIN_GRADIENT
1062
+ ) {
1063
+ return null;
1064
+ }
1065
+
1066
+ let best = null;
1067
+ for (const preset of KNOWN_48_FLAT_FILL_PRESETS) {
1068
+ const filled = applyFlatBackgroundFill({
1069
+ sourceImageData,
1070
+ alphaMap,
1071
+ position,
1072
+ ...preset
1073
+ });
1074
+ if (!filled) continue;
1075
+
1076
+ const spatialScore = computeRegionSpatialCorrelation({
1077
+ imageData: filled.imageData,
1078
+ alphaMap,
1079
+ region: { x: position.x, y: position.y, size: position.width }
1080
+ });
1081
+ const gradientScore = computeRegionGradientCorrelation({
1082
+ imageData: filled.imageData,
1083
+ alphaMap,
1084
+ region: { x: position.x, y: position.y, size: position.width }
1085
+ });
1086
+ const gradientImprovement = baselineGradientScore - gradientScore;
1087
+ const keptSpatial = Math.abs(spatialScore) <= Math.abs(baselineSpatialScore) + KNOWN_48_FLAT_FILL_MAX_SPATIAL_DRIFT;
1088
+ const acceptedSpatial = Math.abs(spatialScore) <= KNOWN_48_FLAT_FILL_MAX_ACCEPTED_ABS_SPATIAL;
1089
+ if (
1090
+ gradientImprovement < minGradientImprovement ||
1091
+ !keptSpatial ||
1092
+ !acceptedSpatial
1093
+ ) {
1094
+ continue;
1095
+ }
1096
+
1097
+ const cost = Math.abs(spatialScore) * 0.45 + Math.max(0, gradientScore);
1098
+ if (!best || cost < best.cost) {
1099
+ best = {
1100
+ imageData: filled.imageData,
1101
+ spatialScore,
1102
+ gradientScore,
1103
+ stats: filled.stats,
1104
+ preset: preset.name,
1105
+ cost
1106
+ };
1107
+ }
1108
+ }
1109
+
1110
+ return best;
1111
+ }
1112
+
1113
+ function isNewMargin96AlphaVariantConfig(config) {
1114
+ return config?.logoSize === 96 &&
1115
+ config.marginRight === 192 &&
1116
+ config.marginBottom === 192 &&
1117
+ config.alphaVariant === '20260520';
1118
+ }
1119
+
1120
+ function refineNewMargin96FlatBackgroundResidual({
1121
+ sourceImageData,
1122
+ alphaMap,
1123
+ position,
1124
+ config,
1125
+ alphaGain,
1126
+ baselineSpatialScore,
1127
+ baselineGradientScore
1128
+ }) {
1129
+ if (
1130
+ !isNewMargin96AlphaVariantConfig(config) ||
1131
+ alphaGain !== 1 ||
1132
+ position?.width !== 96 ||
1133
+ baselineGradientScore < NEW_MARGIN_96_FLAT_FILL_MIN_GRADIENT
1134
+ ) {
1135
+ return null;
1136
+ }
1137
+
1138
+ let best = null;
1139
+ for (const preset of NEW_MARGIN_96_FLAT_FILL_PRESETS) {
1140
+ const filled = applyFlatBackgroundFill({
1141
+ sourceImageData,
1142
+ alphaMap,
1143
+ position,
1144
+ maxBackgroundStd: NEW_MARGIN_96_FLAT_FILL_MAX_BACKGROUND_STD,
1145
+ edgeWeightFloor: 0.85,
1146
+ ...preset
1147
+ });
1148
+ if (!filled) continue;
1149
+
1150
+ const spatialScore = computeRegionSpatialCorrelation({
1151
+ imageData: filled.imageData,
1152
+ alphaMap,
1153
+ region: { x: position.x, y: position.y, size: position.width }
1154
+ });
1155
+ const gradientScore = computeRegionGradientCorrelation({
1156
+ imageData: filled.imageData,
1157
+ alphaMap,
1158
+ region: { x: position.x, y: position.y, size: position.width }
1159
+ });
1160
+ const gradientImprovement = baselineGradientScore - gradientScore;
1161
+ const keptSpatial = Math.abs(spatialScore) <= Math.abs(baselineSpatialScore) + NEW_MARGIN_96_FLAT_FILL_MAX_SPATIAL_DRIFT;
1162
+ const acceptedSpatial = Math.abs(spatialScore) <= NEW_MARGIN_96_FLAT_FILL_MAX_ACCEPTED_ABS_SPATIAL;
1163
+ if (
1164
+ gradientImprovement < NEW_MARGIN_96_FLAT_FILL_MIN_GRADIENT_IMPROVEMENT ||
1165
+ !keptSpatial ||
1166
+ !acceptedSpatial
1167
+ ) {
1168
+ continue;
1169
+ }
1170
+
1171
+ const cost = Math.abs(spatialScore) * 0.6 + Math.max(0, gradientScore);
1172
+ if (!best || cost < best.cost) {
1173
+ best = {
1174
+ imageData: filled.imageData,
1175
+ spatialScore,
1176
+ gradientScore,
1177
+ stats: filled.stats,
1178
+ preset: preset.name,
1179
+ cost
1180
+ };
1181
+ }
1182
+ }
1183
+
1184
+ return best;
1185
+ }
1186
+
1187
+ function pixelLuminance(data, index) {
1188
+ return 0.2126 * data[index] + 0.7152 * data[index + 1] + 0.0722 * data[index + 2];
1189
+ }
1190
+
1191
+ function applyLumaEdgeCorrection({
1192
+ sourceImageData,
1193
+ alphaMap,
1194
+ position,
1195
+ minAlpha,
1196
+ maxAlpha,
1197
+ referenceAlphaMax,
1198
+ radius,
1199
+ strength,
1200
+ colorSigma,
1201
+ maxDelta
1202
+ }) {
1203
+ const candidate = cloneImageData(sourceImageData);
1204
+ const { data, width: imageWidth, height: imageHeight } = sourceImageData;
1205
+ const size = position.width;
1206
+ const colorSigmaSafe = Math.max(1, colorSigma);
1207
+ const edgeMask = createAlphaGradientMask({
1208
+ alphaMap,
1209
+ width: size,
1210
+ height: size
1211
+ });
1212
+
1213
+ for (let row = 0; row < size; row++) {
1214
+ for (let col = 0; col < size; col++) {
1215
+ const localIndex = row * size + col;
1216
+ const alpha = alphaMap[localIndex];
1217
+ if (alpha < minAlpha || alpha > maxAlpha) continue;
1218
+
1219
+ const x = position.x + col;
1220
+ const y = position.y + row;
1221
+ const pixelIndex = (y * imageWidth + x) * 4;
1222
+ const currentLum = pixelLuminance(data, pixelIndex);
1223
+ let weightedLum = 0;
1224
+ let sumWeight = 0;
1225
+
1226
+ for (let dy = -radius; dy <= radius; dy++) {
1227
+ for (let dx = -radius; dx <= radius; dx++) {
1228
+ if (dx === 0 && dy === 0) continue;
1229
+ const distanceSquared = dx * dx + dy * dy;
1230
+ if (distanceSquared > radius * radius) continue;
1231
+
1232
+ const localX = col + dx;
1233
+ const localY = row + dy;
1234
+ const pixelX = x + dx;
1235
+ const pixelY = y + dy;
1236
+ if (pixelX < 0 || pixelY < 0 || pixelX >= imageWidth || pixelY >= imageHeight) continue;
1237
+
1238
+ let neighborAlpha = 0;
1239
+ if (localX >= 0 && localY >= 0 && localX < size && localY < size) {
1240
+ neighborAlpha = alphaMap[localY * size + localX];
1241
+ }
1242
+ if (neighborAlpha > referenceAlphaMax && neighborAlpha >= alpha) continue;
1243
+
1244
+ const neighborIndex = (pixelY * imageWidth + pixelX) * 4;
1245
+ const neighborLum = pixelLuminance(data, neighborIndex);
1246
+ const colorDistance =
1247
+ Math.abs(data[pixelIndex] - data[neighborIndex]) +
1248
+ Math.abs(data[pixelIndex + 1] - data[neighborIndex + 1]) +
1249
+ Math.abs(data[pixelIndex + 2] - data[neighborIndex + 2]);
1250
+ const colorWeight = Math.exp(
1251
+ -(colorDistance * colorDistance) / (2 * colorSigmaSafe * colorSigmaSafe * 9)
1252
+ );
1253
+ const alphaWeight = neighborAlpha <= referenceAlphaMax ? 1.25 : 0.65;
1254
+ const distanceWeight = 1 / Math.sqrt(distanceSquared);
1255
+ const weight = colorWeight * alphaWeight * distanceWeight;
1256
+ weightedLum += neighborLum * weight;
1257
+ sumWeight += weight;
1258
+ }
1259
+ }
1260
+
1261
+ if (sumWeight <= 0) continue;
1262
+
1263
+ const targetLum = weightedLum / sumWeight;
1264
+ const delta = Math.max(-maxDelta, Math.min(maxDelta, targetLum - currentLum)) * strength;
1265
+ const edgeWeight = getAlphaGradientWeight(edgeMask, localIndex);
1266
+ const scaledStrength = Math.min(1, Math.max(0, alpha / Math.max(maxAlpha, 1e-6))) * edgeWeight;
1267
+ const finalDelta = delta * scaledStrength;
1268
+ candidate.data[pixelIndex] = clampChannel(data[pixelIndex] + finalDelta);
1269
+ candidate.data[pixelIndex + 1] = clampChannel(data[pixelIndex + 1] + finalDelta);
1270
+ candidate.data[pixelIndex + 2] = clampChannel(data[pixelIndex + 2] + finalDelta);
1271
+ }
1272
+ }
1273
+
1274
+ return candidate;
1275
+ }
1276
+
1277
+ function refineKnown48LumaEdgeResidual({
1278
+ sourceImageData,
1279
+ alphaMap,
1280
+ position,
1281
+ baselineSpatialScore,
1282
+ baselineGradientScore
1283
+ }) {
1284
+ if (
1285
+ position?.width < KNOWN_48_EDGE_CLEANUP_MIN_SIZE ||
1286
+ position?.width > KNOWN_48_EDGE_CLEANUP_MAX_SIZE ||
1287
+ baselineGradientScore < KNOWN_48_LUMA_EDGE_MIN_GRADIENT
1288
+ ) {
1289
+ return null;
1290
+ }
1291
+
1292
+ let best = null;
1293
+ for (const preset of KNOWN_48_LUMA_EDGE_PRESETS) {
1294
+ const candidate = applyLumaEdgeCorrection({
1295
+ sourceImageData,
1296
+ alphaMap,
1297
+ position,
1298
+ ...preset
1299
+ });
1300
+ const spatialScore = computeRegionSpatialCorrelation({
1301
+ imageData: candidate,
1302
+ alphaMap,
1303
+ region: { x: position.x, y: position.y, size: position.width }
1304
+ });
1305
+ const gradientScore = computeRegionGradientCorrelation({
1306
+ imageData: candidate,
1307
+ alphaMap,
1308
+ region: { x: position.x, y: position.y, size: position.width }
1309
+ });
1310
+ const gradientImprovement = baselineGradientScore - gradientScore;
1311
+ const keptSpatial = Math.abs(spatialScore) <= Math.abs(baselineSpatialScore) + KNOWN_48_LUMA_EDGE_MAX_SPATIAL_DRIFT;
1312
+ const acceptedSpatial = Math.abs(spatialScore) <= KNOWN_48_LUMA_EDGE_MAX_ACCEPTED_ABS_SPATIAL;
1313
+ if (
1314
+ gradientImprovement < KNOWN_48_LUMA_EDGE_MIN_GRADIENT_IMPROVEMENT ||
1315
+ !keptSpatial ||
1316
+ !acceptedSpatial
1317
+ ) {
1318
+ continue;
1319
+ }
1320
+
1321
+ const cost = Math.abs(spatialScore) * 0.45 + Math.max(0, gradientScore);
1322
+ if (!best || cost < best.cost) {
1323
+ best = {
1324
+ imageData: candidate,
1325
+ spatialScore,
1326
+ gradientScore,
1327
+ preset: preset.name,
1328
+ cost
1329
+ };
1330
+ }
1331
+ }
1332
+
1333
+ return best;
714
1334
  }
715
1335
 
716
1336
  function expandPosition(position, imageData, pad) {
@@ -1006,10 +1626,11 @@ function refinePreviewResidualEdge({
1006
1626
  position,
1007
1627
  source,
1008
1628
  baselineSpatialScore,
1009
- baselineGradientScore,
1629
+ baselineGradientScore,
1010
1630
  minGradientImprovement = PREVIEW_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT,
1011
1631
  maxSpatialDrift = PREVIEW_EDGE_CLEANUP_MAX_SPATIAL_DRIFT,
1012
- allowAggressivePresets = false
1632
+ allowAggressivePresets = false,
1633
+ mode = 'preview'
1013
1634
  }) {
1014
1635
  const baselineHalo = assessAlphaBandHalo({
1015
1636
  imageData: sourceImageData,
@@ -1017,12 +1638,13 @@ function refinePreviewResidualEdge({
1017
1638
  alphaMap
1018
1639
  });
1019
1640
  const baselinePositiveHalo = baselineHalo.positiveDeltaLum;
1020
- if (!shouldRefinePreviewResidualEdge({
1641
+ if (!shouldRefineResidualEdge({
1021
1642
  source,
1022
1643
  position,
1023
1644
  baselineSpatialScore,
1024
1645
  baselineGradientScore,
1025
- baselinePositiveHalo
1646
+ baselinePositiveHalo,
1647
+ mode
1026
1648
  })) {
1027
1649
  return null;
1028
1650
  }
@@ -1093,10 +1715,141 @@ function refinePreviewResidualEdge({
1093
1715
  };
1094
1716
  }
1095
1717
  }
1096
-
1097
- return best;
1098
- }
1099
-
1718
+
1719
+ return best;
1720
+ }
1721
+
1722
+ function scoreLocatedAggressiveCandidate({
1723
+ imageData,
1724
+ alphaMap,
1725
+ position
1726
+ }) {
1727
+ const spatialScore = computeRegionSpatialCorrelation({
1728
+ imageData,
1729
+ alphaMap,
1730
+ region: { x: position.x, y: position.y, size: position.width }
1731
+ });
1732
+ const gradientScore = computeRegionGradientCorrelation({
1733
+ imageData,
1734
+ alphaMap,
1735
+ region: { x: position.x, y: position.y, size: position.width }
1736
+ });
1737
+ const nearBlackRatio = calculateNearBlackRatio(imageData, position);
1738
+ return {
1739
+ spatialScore,
1740
+ gradientScore,
1741
+ nearBlackRatio,
1742
+ cost: Math.abs(spatialScore) * 0.8 + Math.max(0, gradientScore) * 0.75
1743
+ };
1744
+ }
1745
+
1746
+ function pickLocatedAggressiveCandidate(currentBest, candidate) {
1747
+ if (!candidate) return currentBest;
1748
+ if (!currentBest || candidate.cost < currentBest.cost) return candidate;
1749
+ return currentBest;
1750
+ }
1751
+
1752
+ function buildLocatedAggressiveCandidate({
1753
+ sourceImageData,
1754
+ alphaMap,
1755
+ position,
1756
+ alphaGain,
1757
+ repeatCount
1758
+ }) {
1759
+ const candidate = cloneImageData(sourceImageData);
1760
+ for (let passIndex = 0; passIndex < repeatCount; passIndex++) {
1761
+ removeWatermark(candidate, alphaMap, position, { alphaGain });
1762
+ }
1763
+ return {
1764
+ imageData: candidate,
1765
+ alphaGain,
1766
+ repeatCount,
1767
+ ...scoreLocatedAggressiveCandidate({
1768
+ imageData: candidate,
1769
+ alphaMap,
1770
+ position
1771
+ })
1772
+ };
1773
+ }
1774
+
1775
+ function refineLocatedAggressiveRemoval({
1776
+ originalImageData,
1777
+ currentImageData,
1778
+ alphaMap,
1779
+ position,
1780
+ currentSpatialScore,
1781
+ currentGradientScore,
1782
+ currentAlphaGain
1783
+ }) {
1784
+ if (!position || !alphaMap || position.width !== position.height) return null;
1785
+ const current = {
1786
+ imageData: currentImageData,
1787
+ alphaGain: currentAlphaGain,
1788
+ repeatCount: 0,
1789
+ spatialScore: currentSpatialScore,
1790
+ gradientScore: currentGradientScore,
1791
+ nearBlackRatio: calculateNearBlackRatio(currentImageData, position),
1792
+ cost: Math.abs(currentSpatialScore) * 0.8 + Math.max(0, currentGradientScore) * 0.75
1793
+ };
1794
+ let best = current;
1795
+ const gains = new Set([
1796
+ currentAlphaGain,
1797
+ ...LOCATED_AGGRESSIVE_ALPHA_GAINS
1798
+ ].filter((value) => Number.isFinite(value) && value > 0));
1799
+
1800
+ for (const alphaGain of gains) {
1801
+ for (const repeatCount of [1, 2]) {
1802
+ best = pickLocatedAggressiveCandidate(
1803
+ best,
1804
+ buildLocatedAggressiveCandidate({
1805
+ sourceImageData: originalImageData,
1806
+ alphaMap,
1807
+ position,
1808
+ alphaGain,
1809
+ repeatCount
1810
+ })
1811
+ );
1812
+ }
1813
+ }
1814
+
1815
+ const edgeSources = [{
1816
+ imageData: best.imageData,
1817
+ alphaGain: best.alphaGain,
1818
+ repeatCount: best.repeatCount
1819
+ }];
1820
+ if (best.imageData !== currentImageData) {
1821
+ edgeSources.push({
1822
+ imageData: currentImageData,
1823
+ alphaGain: currentAlphaGain,
1824
+ repeatCount: 0
1825
+ });
1826
+ }
1827
+ for (const edgeSource of edgeSources) {
1828
+ for (const preset of LOCATED_AGGRESSIVE_EDGE_PRESETS) {
1829
+ const edgeCandidate = blendPreviewResidualEdge({
1830
+ sourceImageData: edgeSource.imageData,
1831
+ alphaMap,
1832
+ position,
1833
+ ...preset
1834
+ });
1835
+ best = pickLocatedAggressiveCandidate(best, {
1836
+ imageData: edgeCandidate,
1837
+ alphaGain: edgeSource.alphaGain,
1838
+ repeatCount: edgeSource.repeatCount,
1839
+ edgeCleanup: true,
1840
+ ...scoreLocatedAggressiveCandidate({
1841
+ imageData: edgeCandidate,
1842
+ alphaMap,
1843
+ position
1844
+ })
1845
+ });
1846
+ }
1847
+ }
1848
+
1849
+ if (best.imageData === currentImageData) return null;
1850
+ return best;
1851
+ }
1852
+
1100
1853
  export function processWatermarkImageData(imageData, options = {}) {
1101
1854
  const totalStartedAt = nowMs();
1102
1855
  const debugTimingsEnabled = options.debugTimings === true;
@@ -1145,10 +1898,11 @@ export function processWatermarkImageData(imageData, options = {}) {
1145
1898
  afterSpatialScore,
1146
1899
  afterGradientScore,
1147
1900
  suppressionGain: stageSuppressionGain = null,
1148
- cost = null
1901
+ cost = null,
1902
+ allowSameAlphaGain = false
1149
1903
  }) => {
1150
1904
  if (!stage || !Number.isFinite(fromAlphaGain) || !Number.isFinite(toAlphaGain)) return;
1151
- if (Math.abs(fromAlphaGain - toAlphaGain) < 0.0001) return;
1905
+ if (!allowSameAlphaGain && Math.abs(fromAlphaGain - toAlphaGain) < 0.0001) return;
1152
1906
 
1153
1907
  alphaAdjustmentStages.push({
1154
1908
  stage,
@@ -1162,12 +1916,12 @@ export function processWatermarkImageData(imageData, options = {}) {
1162
1916
  cost: Number.isFinite(cost) ? cost : null
1163
1917
  });
1164
1918
  };
1165
-
1166
- const initialSelectionStartedAt = nowMs();
1167
- const initialSelection = selectInitialCandidate({
1168
- originalImageData,
1169
- config,
1170
- position,
1919
+
1920
+ const initialSelectionStartedAt = nowMs();
1921
+ let initialSelection = selectInitialCandidate({
1922
+ originalImageData,
1923
+ config,
1924
+ position,
1171
1925
  alpha48,
1172
1926
  alpha96,
1173
1927
  alpha96Variants: options.alpha96Variants ?? null,
@@ -1177,9 +1931,37 @@ export function processWatermarkImageData(imageData, options = {}) {
1177
1931
  alphaGainCandidates,
1178
1932
  alphaPriorityGains
1179
1933
  });
1180
- if (debugTimingsEnabled) {
1181
- debugTimings.initialSelectionMs = nowMs() - initialSelectionStartedAt;
1182
- }
1934
+ if (
1935
+ !initialSelection.selectedTrial &&
1936
+ options.aggressiveLocatedFallback !== false
1937
+ ) {
1938
+ const aggressiveSelection = selectInitialCandidate({
1939
+ originalImageData,
1940
+ config,
1941
+ position,
1942
+ alpha48,
1943
+ alpha96,
1944
+ alpha96Variants: options.alpha96Variants ?? null,
1945
+ getAlphaMap: options.getAlphaMap,
1946
+ allowAdaptiveSearch,
1947
+ allowAutomaticSearch: true,
1948
+ allowAggressiveStrongLocated: true,
1949
+ alphaGainCandidates,
1950
+ alphaPriorityGains
1951
+ });
1952
+ if (aggressiveSelection.selectedTrial) {
1953
+ initialSelection = {
1954
+ ...aggressiveSelection,
1955
+ source: aggressiveSelection.source.includes('aggressive-located')
1956
+ ? aggressiveSelection.source
1957
+ : `${aggressiveSelection.source}+aggressive-located`,
1958
+ decisionTier: aggressiveSelection.decisionTier || 'direct-match'
1959
+ };
1960
+ }
1961
+ }
1962
+ if (debugTimingsEnabled) {
1963
+ debugTimings.initialSelectionMs = nowMs() - initialSelectionStartedAt;
1964
+ }
1183
1965
 
1184
1966
  if (!initialSelection.selectedTrial) {
1185
1967
  if (debugTimingsEnabled) {
@@ -1216,6 +1998,16 @@ export function processWatermarkImageData(imageData, options = {}) {
1216
1998
 
1217
1999
  const selectedTrial = initialSelection.selectedTrial;
1218
2000
  const usePreviewAnchorFastCleanup = shouldUsePreviewAnchorFastCleanup(selectedTrial, position);
2001
+ const useKnown48EdgeCleanup = shouldUseKnown48EdgeCleanup({
2002
+ selectedTrial,
2003
+ position,
2004
+ source
2005
+ });
2006
+ const useV2SmallEdgeCleanup = shouldUseV2SmallEdgeCleanup({
2007
+ selectedTrial,
2008
+ position,
2009
+ source
2010
+ });
1219
2011
 
1220
2012
  let finalImageData = selectedTrial.imageData;
1221
2013
 
@@ -1418,6 +2210,7 @@ export function processWatermarkImageData(imageData, options = {}) {
1418
2210
  currentGradientScore: finalProcessedGradientScore,
1419
2211
  currentAlphaGain: alphaGain,
1420
2212
  originalSpatialScore,
2213
+ originalGradientScore,
1421
2214
  originalNearBlackRatio: calculateNearBlackRatio(originalImageData, position)
1422
2215
  });
1423
2216
  if (weakAlphaFineTune) {
@@ -1503,7 +2296,24 @@ export function processWatermarkImageData(imageData, options = {}) {
1503
2296
  source,
1504
2297
  baselineSpatialScore: finalProcessedSpatialScore,
1505
2298
  baselineGradientScore: finalProcessedGradientScore,
1506
- allowAggressivePresets: usePreviewAnchorFastCleanup
2299
+ minGradientImprovement: useKnown48EdgeCleanup
2300
+ ? KNOWN_48_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT
2301
+ : (
2302
+ useV2SmallEdgeCleanup
2303
+ ? V2_SMALL_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT
2304
+ : PREVIEW_EDGE_CLEANUP_MIN_GRADIENT_IMPROVEMENT
2305
+ ),
2306
+ maxSpatialDrift: useKnown48EdgeCleanup
2307
+ ? KNOWN_48_EDGE_CLEANUP_MAX_SPATIAL_DRIFT
2308
+ : (
2309
+ useV2SmallEdgeCleanup
2310
+ ? V2_SMALL_EDGE_CLEANUP_MAX_SPATIAL_DRIFT
2311
+ : PREVIEW_EDGE_CLEANUP_MAX_SPATIAL_DRIFT
2312
+ ),
2313
+ allowAggressivePresets: usePreviewAnchorFastCleanup,
2314
+ mode: useKnown48EdgeCleanup
2315
+ ? 'known-48'
2316
+ : (useV2SmallEdgeCleanup ? 'v2-small' : 'preview')
1507
2317
  });
1508
2318
  previewEdgeCleanupElapsedMs += nowMs() - previewEdgeStartedAt;
1509
2319
 
@@ -1511,13 +2321,13 @@ export function processWatermarkImageData(imageData, options = {}) {
1511
2321
  return false;
1512
2322
  }
1513
2323
 
1514
- finalImageData = previewEdgeRefined.imageData;
1515
- finalProcessedSpatialScore = previewEdgeRefined.spatialScore;
1516
- finalProcessedGradientScore = previewEdgeRefined.gradientScore;
1517
- suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
1518
- source = `${source}+edge-cleanup`;
1519
- return true;
1520
- };
2324
+ finalImageData = previewEdgeRefined.imageData;
2325
+ finalProcessedSpatialScore = previewEdgeRefined.spatialScore;
2326
+ finalProcessedGradientScore = previewEdgeRefined.gradientScore;
2327
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
2328
+ source = `${source}+${useV2SmallEdgeCleanup ? 'v2-small-edge-cleanup' : 'edge-cleanup'}`;
2329
+ return true;
2330
+ };
1521
2331
 
1522
2332
  const subpixelStartedAt = nowMs();
1523
2333
  if (
@@ -1570,13 +2380,111 @@ export function processWatermarkImageData(imageData, options = {}) {
1570
2380
  }
1571
2381
 
1572
2382
  let previewEdgeCleanupPassCount = 0;
1573
- while (ENABLE_VISUAL_POST_PROCESSING && previewEdgeCleanupPassCount < PREVIEW_EDGE_CLEANUP_MAX_APPLIED_PASSES) {
2383
+ const shouldRunEdgeCleanup = ENABLE_VISUAL_POST_PROCESSING || useKnown48EdgeCleanup || useV2SmallEdgeCleanup;
2384
+ while (shouldRunEdgeCleanup && previewEdgeCleanupPassCount < PREVIEW_EDGE_CLEANUP_MAX_APPLIED_PASSES) {
1574
2385
  if (!applyPreviewEdgeCleanup()) {
1575
2386
  break;
1576
2387
  }
1577
2388
  previewEdgeCleanupPassCount++;
1578
2389
  }
1579
2390
 
2391
+ if (useKnown48EdgeCleanup) {
2392
+ let flatFillPassCount = 0;
2393
+ while (flatFillPassCount < KNOWN_48_FLAT_FILL_MAX_APPLIED_PASSES) {
2394
+ const flatBackgroundRefined = refineKnown48FlatBackgroundResidual({
2395
+ sourceImageData: finalImageData,
2396
+ alphaMap,
2397
+ position,
2398
+ baselineSpatialScore: finalProcessedSpatialScore,
2399
+ baselineGradientScore: finalProcessedGradientScore,
2400
+ minGradientImprovement: flatFillPassCount === 0
2401
+ ? KNOWN_48_FLAT_FILL_MIN_GRADIENT_IMPROVEMENT
2402
+ : KNOWN_48_FLAT_FILL_SECOND_PASS_MIN_GRADIENT_IMPROVEMENT
2403
+ });
2404
+
2405
+ if (!flatBackgroundRefined) {
2406
+ break;
2407
+ }
2408
+
2409
+ recordAlphaAdjustmentStage({
2410
+ stage: 'known-48-flat-background-fill',
2411
+ fromAlphaGain: alphaGain,
2412
+ toAlphaGain: alphaGain,
2413
+ beforeSpatialScore: finalProcessedSpatialScore,
2414
+ beforeGradientScore: finalProcessedGradientScore,
2415
+ afterSpatialScore: flatBackgroundRefined.spatialScore,
2416
+ afterGradientScore: flatBackgroundRefined.gradientScore,
2417
+ suppressionGain: originalSpatialScore - flatBackgroundRefined.spatialScore,
2418
+ cost: flatBackgroundRefined.cost,
2419
+ allowSameAlphaGain: true
2420
+ });
2421
+ finalImageData = flatBackgroundRefined.imageData;
2422
+ finalProcessedSpatialScore = flatBackgroundRefined.spatialScore;
2423
+ finalProcessedGradientScore = flatBackgroundRefined.gradientScore;
2424
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
2425
+ source = `${source}+flat-fill`;
2426
+ flatFillPassCount++;
2427
+ }
2428
+
2429
+ const lumaEdgeRefined = refineKnown48LumaEdgeResidual({
2430
+ sourceImageData: finalImageData,
2431
+ alphaMap,
2432
+ position,
2433
+ baselineSpatialScore: finalProcessedSpatialScore,
2434
+ baselineGradientScore: finalProcessedGradientScore
2435
+ });
2436
+
2437
+ if (lumaEdgeRefined) {
2438
+ recordAlphaAdjustmentStage({
2439
+ stage: 'known-48-luma-edge-correction',
2440
+ fromAlphaGain: alphaGain,
2441
+ toAlphaGain: alphaGain,
2442
+ beforeSpatialScore: finalProcessedSpatialScore,
2443
+ beforeGradientScore: finalProcessedGradientScore,
2444
+ afterSpatialScore: lumaEdgeRefined.spatialScore,
2445
+ afterGradientScore: lumaEdgeRefined.gradientScore,
2446
+ suppressionGain: originalSpatialScore - lumaEdgeRefined.spatialScore,
2447
+ cost: lumaEdgeRefined.cost,
2448
+ allowSameAlphaGain: true
2449
+ });
2450
+ finalImageData = lumaEdgeRefined.imageData;
2451
+ finalProcessedSpatialScore = lumaEdgeRefined.spatialScore;
2452
+ finalProcessedGradientScore = lumaEdgeRefined.gradientScore;
2453
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
2454
+ source = `${source}+luma-edge`;
2455
+ }
2456
+ }
2457
+
2458
+ const newMargin96FlatFillRefined = refineNewMargin96FlatBackgroundResidual({
2459
+ sourceImageData: finalImageData,
2460
+ alphaMap,
2461
+ position,
2462
+ config,
2463
+ alphaGain,
2464
+ baselineSpatialScore: finalProcessedSpatialScore,
2465
+ baselineGradientScore: finalProcessedGradientScore
2466
+ });
2467
+
2468
+ if (newMargin96FlatFillRefined) {
2469
+ recordAlphaAdjustmentStage({
2470
+ stage: 'new-margin-96-flat-background-fill',
2471
+ fromAlphaGain: alphaGain,
2472
+ toAlphaGain: alphaGain,
2473
+ beforeSpatialScore: finalProcessedSpatialScore,
2474
+ beforeGradientScore: finalProcessedGradientScore,
2475
+ afterSpatialScore: newMargin96FlatFillRefined.spatialScore,
2476
+ afterGradientScore: newMargin96FlatFillRefined.gradientScore,
2477
+ suppressionGain: originalSpatialScore - newMargin96FlatFillRefined.spatialScore,
2478
+ cost: newMargin96FlatFillRefined.cost,
2479
+ allowSameAlphaGain: true
2480
+ });
2481
+ finalImageData = newMargin96FlatFillRefined.imageData;
2482
+ finalProcessedSpatialScore = newMargin96FlatFillRefined.spatialScore;
2483
+ finalProcessedGradientScore = newMargin96FlatFillRefined.gradientScore;
2484
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
2485
+ source = `${source}+flat-fill`;
2486
+ }
2487
+
1580
2488
  const smallPreviewRefinementStartedAt = nowMs();
1581
2489
  const smallPreviewRefined = ENABLE_VISUAL_POST_PROCESSING
1582
2490
  ? refineSmallPreviewAnchorCandidate({
@@ -1617,25 +2525,83 @@ export function processWatermarkImageData(imageData, options = {}) {
1617
2525
  suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
1618
2526
  source = `${source}+small-preview-refine`;
1619
2527
  }
2528
+
2529
+ const locatedAggressiveStartedAt = nowMs();
2530
+ if (options.locatedAggressiveRemoval !== false) {
2531
+ const aggressiveRefined = refineLocatedAggressiveRemoval({
2532
+ originalImageData,
2533
+ currentImageData: finalImageData,
2534
+ alphaMap,
2535
+ position,
2536
+ currentSpatialScore: finalProcessedSpatialScore,
2537
+ currentGradientScore: finalProcessedGradientScore,
2538
+ currentAlphaGain: alphaGain
2539
+ });
2540
+ if (aggressiveRefined) {
2541
+ recordAlphaAdjustmentStage({
2542
+ stage: 'located-aggressive-removal',
2543
+ fromAlphaGain: alphaGain,
2544
+ toAlphaGain: aggressiveRefined.alphaGain,
2545
+ beforeSpatialScore: finalProcessedSpatialScore,
2546
+ beforeGradientScore: finalProcessedGradientScore,
2547
+ afterSpatialScore: aggressiveRefined.spatialScore,
2548
+ afterGradientScore: aggressiveRefined.gradientScore,
2549
+ suppressionGain: originalSpatialScore - aggressiveRefined.spatialScore,
2550
+ cost: aggressiveRefined.cost,
2551
+ allowSameAlphaGain: true
2552
+ });
2553
+ passes.push({
2554
+ index: passes.length + 1,
2555
+ beforeSpatialScore: finalProcessedSpatialScore,
2556
+ beforeGradientScore: finalProcessedGradientScore,
2557
+ afterSpatialScore: aggressiveRefined.spatialScore,
2558
+ afterGradientScore: aggressiveRefined.gradientScore,
2559
+ improvement: Math.abs(finalProcessedSpatialScore) - Math.abs(aggressiveRefined.spatialScore),
2560
+ gradientDelta: aggressiveRefined.gradientScore - finalProcessedGradientScore,
2561
+ nearBlackRatio: aggressiveRefined.nearBlackRatio
2562
+ });
2563
+ finalImageData = aggressiveRefined.imageData;
2564
+ alphaGain = aggressiveRefined.alphaGain;
2565
+ finalProcessedSpatialScore = aggressiveRefined.spatialScore;
2566
+ finalProcessedGradientScore = aggressiveRefined.gradientScore;
2567
+ suppressionGain = originalSpatialScore - finalProcessedSpatialScore;
2568
+ passCount += Math.max(1, aggressiveRefined.repeatCount || 1);
2569
+ attemptedPassCount += Math.max(1, aggressiveRefined.repeatCount || 1);
2570
+ passStopReason = aggressiveRefined.edgeCleanup
2571
+ ? 'located-aggressive-edge-cleanup'
2572
+ : 'located-aggressive-alpha';
2573
+ source = source.includes('+located-aggressive')
2574
+ ? source
2575
+ : `${source}+located-aggressive`;
2576
+ }
2577
+ }
1620
2578
  if (debugTimingsEnabled) {
1621
2579
  debugTimings.previewEdgeCleanupMs = previewEdgeCleanupElapsedMs;
1622
2580
  debugTimings.smallPreviewRefinementMs = nowMs() - smallPreviewRefinementStartedAt;
2581
+ debugTimings.locatedAggressiveRemovalMs = nowMs() - locatedAggressiveStartedAt;
1623
2582
  debugTimings.totalMs = nowMs() - totalStartedAt;
1624
2583
  }
1625
-
1626
- return {
1627
- imageData: finalImageData,
1628
- meta: createWatermarkMeta({
2584
+
2585
+ const residualVisibility = assessWatermarkResidualVisibility({
2586
+ imageData: finalImageData,
2587
+ position,
2588
+ alphaMap
2589
+ });
2590
+
2591
+ return {
2592
+ imageData: finalImageData,
2593
+ meta: createWatermarkMeta({
1629
2594
  position,
1630
2595
  config,
1631
2596
  adaptiveConfidence,
1632
2597
  originalSpatialScore,
1633
- originalGradientScore,
1634
- processedSpatialScore: finalProcessedSpatialScore,
1635
- processedGradientScore: finalProcessedGradientScore,
1636
- suppressionGain,
1637
- templateWarp,
1638
- alphaGain,
2598
+ originalGradientScore,
2599
+ processedSpatialScore: finalProcessedSpatialScore,
2600
+ processedGradientScore: finalProcessedGradientScore,
2601
+ suppressionGain,
2602
+ residualVisibility,
2603
+ templateWarp,
2604
+ alphaGain,
1639
2605
  passCount,
1640
2606
  attemptedPassCount,
1641
2607
  passStopReason,