@pilio/gemini-watermark-remover 1.0.42 → 1.0.43

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/dist/video-app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pilio/gemini-watermark-remover v1.0.42+unknown
2
+ * @pilio/gemini-watermark-remover v1.0.43+unknown
3
3
  * Automatically removes watermarks from Gemini AI generated images
4
4
  * (c) 2026 GargantuaX
5
5
  * https://github.com/GargantuaX/gemini-watermark-remover
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.42",
4
+ "version": "1.0.43",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -13,6 +13,7 @@ import {
13
13
  } from './previewAlphaCalibration.js';
14
14
  import { compareRankingKey } from './watermarkScoring.js';
15
15
  import { shouldPreferFullStrengthNewMarginVariant } from './candidateEvaluation.js';
16
+ import { measureRowAlphaEvidence, supportsRowAlphaGain } from './rowAlphaEvidence.js';
16
17
 
17
18
  export const FINAL_EVIDENCE_WEIGHT = 0.35;
18
19
  export const FINAL_RESIDUAL_WEIGHT = 0.40;
@@ -779,7 +780,21 @@ export function rankCompletedCandidates(completed = []) {
779
780
  }
780
781
  scored.sort(compareRankedCandidates);
781
782
  const imperfectionPreferred = applySameAnchor96ImperfectionPreference(scored);
782
- const preferred = applyFullStrengthNewMarginPreference(imperfectionPreferred);
783
+ let preferred = applyFullStrengthNewMarginPreference(imperfectionPreferred);
784
+ const incumbent = preferred[0];
785
+ const rowSupported = preferred.find(candidate => {
786
+ const trial = candidate.hypothesis?.trial;
787
+ if (candidate === incumbent || !supportsRowAlphaGain(trial?.provenance?.rowAlphaEvidence) ||
788
+ candidate.result?.meta?.alphaGain !== 0.6 ||
789
+ !hasSameCandidateAnchor(candidate, incumbent)) return false;
790
+ const before = measureRowAlphaEvidence(incumbent.result?.imageData, trial.position, trial.alphaMap);
791
+ const after = measureRowAlphaEvidence(candidate.result?.imageData, trial.position, trial.alphaMap);
792
+ // Existing cleanup can leave a nonuniform residual. Require positive
793
+ // evidence in every quadrant, but validate the proposed output's fit.
794
+ return before?.gain > 0.15 &&
795
+ before.quadrants.every(value => value > 0.1) && supportsRowAlphaGain(after, 0);
796
+ });
797
+ if (rowSupported) preferred = [rowSupported, ...preferred.filter(candidate => candidate !== rowSupported)];
783
798
 
784
799
  const first = preferred[0];
785
800
  const second = preferred[1];
@@ -15,6 +15,8 @@ import { resolveGeminiWatermarkSearchCatalogEntries } from './geminiSizeCatalog.
15
15
  import { measureOutputResidualLocalization } from './outputResidualLocalization.js';
16
16
  import { hasReliableStandardWatermarkSignal } from './watermarkPresence.js';
17
17
  import { shouldPreferFullStrengthNewMarginVariant } from './candidateEvaluation.js';
18
+ import { measureRowAlphaEvidence, supportsRowAlphaGain } from './rowAlphaEvidence.js';
19
+ import { measurePlaneAlphaEvidence } from './planeAlphaEvidence.js';
18
20
 
19
21
  const AGGRESSIVE_FALLBACK_MAX_ABS_SPATIAL = 0.22;
20
22
  const AGGRESSIVE_FALLBACK_MAX_NEAR_BLACK_INCREASE = 0.05;
@@ -475,6 +477,27 @@ function findCanonical96CatalogPriorTrial({
475
477
  };
476
478
  }
477
479
 
480
+ function createPlaneSourceWitnessTrial(input, fixedSelection, automaticSelection) {
481
+ const image = input.originalImageData;
482
+ // Only this exact-size flat-vector cluster has been independently verified.
483
+ if (image?.width !== 2752 || image.height !== 1536) return null;
484
+ const position = { x: 2464, y: 1248, width: 96, height: 96 };
485
+ if ([fixedSelection?.selectedTrial, automaticSelection?.selectedTrial].some(trial =>
486
+ trial?.accepted === true && trial.position?.width === 96)) return null;
487
+ const alphaMap = input.alpha96Variants?.['20260520'];
488
+ const evidence = measurePlaneAlphaEvidence(image, position, alphaMap);
489
+ if (!evidence) return null;
490
+ const trial = evaluateRestorationCandidate({
491
+ originalImageData: image, alphaMap, position, alphaGain: 1,
492
+ source: 'standard+catalog+plane-source-witness',
493
+ config: { logoSize: 96, marginRight: 192, marginBottom: 192, alphaVariant: '20260520' },
494
+ baselineNearBlackRatio: calculateNearBlackRatio(image, position),
495
+ provenance: { sourceWitnessRescue: true, sourceWitnessReason: 'plane-source-witness', planeSourceEvidence: evidence },
496
+ includeImageData: false
497
+ });
498
+ return measurePresenceLocalization(image, trial).repeatedTemplateCollision ? null : trial;
499
+ }
500
+
478
501
  function selectExact96SourceWitnessTrial({
479
502
  r192Trial,
480
503
  fixedSelection,
@@ -1080,7 +1103,17 @@ export function collectInitialWatermarkCandidates(input = {}) {
1080
1103
  geometryLockWitness.position.x === input.originalImageData.width - 144 &&
1081
1104
  geometryLockWitness.position.y === input.originalImageData.height - 144 &&
1082
1105
  fixedSelection?.selectedTrial?.position?.width !== 48;
1083
- let exact48R96SourceWitnessRescueTrial = presenceConfirmed && !possibleV2Collision && !displacedStrongExact48
1106
+ const rowAlphaTrial = [fixedSelection?.selectedTrial, automaticSelection?.selectedTrial]
1107
+ .find(trial => trial?.position?.width === 48 &&
1108
+ trial.position.x === input.originalImageData.width - 144 &&
1109
+ trial.position.y === input.originalImageData.height - 144 &&
1110
+ trial.alphaGain < 0.6 && !trial.config?.alphaVariant &&
1111
+ trial.provenance?.darkPolarity !== true);
1112
+ const rowAlphaEvidence = rowAlphaTrial
1113
+ ? measureRowAlphaEvidence(input.originalImageData, rowAlphaTrial.position, input.alpha48)
1114
+ : null;
1115
+ const supportedRowAlpha = supportsRowAlphaGain(rowAlphaEvidence);
1116
+ let exact48R96SourceWitnessRescueTrial = presenceConfirmed && !possibleV2Collision && !displacedStrongExact48 && !supportedRowAlpha
1084
1117
  ? null
1085
1118
  : createExact48R96SourceWitnessRescueTrial({
1086
1119
  originalImageData: input.originalImageData,
@@ -1088,6 +1121,9 @@ export function collectInitialWatermarkCandidates(input = {}) {
1088
1121
  config: input.config,
1089
1122
  catalogPriorConfig: input.catalogPriorConfig
1090
1123
  });
1124
+ if (supportedRowAlpha && exact48R96SourceWitnessRescueTrial) {
1125
+ exact48R96SourceWitnessRescueTrial.provenance.rowAlphaEvidence = rowAlphaEvidence;
1126
+ }
1091
1127
  if (possibleV2Collision && exact48R96SourceWitnessRescueTrial) {
1092
1128
  if (
1093
1129
  isGeometryCompatibleWithLock(
@@ -1123,7 +1159,8 @@ export function collectInitialWatermarkCandidates(input = {}) {
1123
1159
  });
1124
1160
  const sourceWitnessRescueTrial =
1125
1161
  exact48R96SourceWitnessRescueTrial ??
1126
- exact96SourceWitnessTrial;
1162
+ exact96SourceWitnessTrial ??
1163
+ createPlaneSourceWitnessTrial(input, fixedSelection, automaticSelection);
1127
1164
  const bestEffortSelections = presenceConfirmed ||
1128
1165
  sourceWitnessRescueTrial
1129
1166
  ? []
@@ -1312,7 +1349,7 @@ export function collectInitialWatermarkCandidates(input = {}) {
1312
1349
  1005
1313
1350
  );
1314
1351
  const hasSupplementalSourceWitness = Boolean(
1315
- displacedStrongExact48 && sourceWitnessRescueHypothesis
1352
+ (displacedStrongExact48 || supportedRowAlpha || sourceWitnessRescueTrial?.provenance?.planeSourceEvidence) && sourceWitnessRescueHypothesis
1316
1353
  );
1317
1354
  const preferredHypotheses = [
1318
1355
  fixedSelectedHypothesis,
@@ -0,0 +1,106 @@
1
+ function median(values) {
2
+ return values.slice().sort((a, b) => a - b)[Math.floor(values.length / 2)];
3
+ }
4
+
5
+ function solvePlane(matrix, values) {
6
+ const rows = matrix.map((row, index) => [...row, values[index]]);
7
+ for (let column = 0; column < 3; column++) {
8
+ let pivot = column;
9
+ for (let row = column + 1; row < 3; row++) {
10
+ if (Math.abs(rows[row][column]) > Math.abs(rows[pivot][column])) pivot = row;
11
+ }
12
+ [rows[column], rows[pivot]] = [rows[pivot], rows[column]];
13
+ if (Math.abs(rows[column][column]) < 1e-10) return null;
14
+ const divisor = rows[column][column];
15
+ for (let index = column; index < 4; index++) rows[column][index] /= divisor;
16
+ for (let row = 0; row < 3; row++) {
17
+ if (row === column) continue;
18
+ const factor = rows[row][column];
19
+ for (let index = column; index < 4; index++) rows[row][index] -= factor * rows[column][index];
20
+ }
21
+ }
22
+ return rows.map(row => row[3]);
23
+ }
24
+
25
+ // Source-only evidence for the confirmed flat-vector 96px profile. The model
26
+ // never fills pixels: the eventual candidate still performs ordinary inverse alpha.
27
+ export function measurePlaneAlphaEvidence(image, position, alphaMap) {
28
+ const size = 96;
29
+ if (!image?.data || alphaMap?.length !== size * size ||
30
+ position?.width !== size || position.height !== size ||
31
+ position.x < 32 || position.y < 32 ||
32
+ position.x + size + 32 > image.width || position.y + size + 32 > image.height) return null;
33
+ const read = (x, y) => {
34
+ const offset = ((position.y + y) * image.width + position.x + x) * 4;
35
+ return [image.data[offset], image.data[offset + 1], image.data[offset + 2]];
36
+ };
37
+ const points = [];
38
+ for (let y = -32; y < size + 32; y += 2) {
39
+ for (let x = -32; x < size + 32; x += 2) {
40
+ if (x >= 0 && x < size && y >= 0 && y < size) continue;
41
+ points.push({ vector: [1, x / size, y / size], rgb: read(x, y) });
42
+ }
43
+ }
44
+ const training = points.filter((_, index) => index % 2 === 0);
45
+ const validation = points.filter((_, index) => index % 2 === 1);
46
+ let coefficients = [0, 1, 2].map(channel => [median(training.map(point => point.rgb[channel])), 0, 0]);
47
+ const predict = vector => coefficients.map(row => row.reduce((sum, value, index) => sum + value * vector[index], 0));
48
+ const error = point => {
49
+ const predicted = predict(point.vector);
50
+ return point.rgb.reduce((sum, value, channel) => sum + Math.abs(value - predicted[channel]), 0) / 3;
51
+ };
52
+ for (let iteration = 0; iteration < 8; iteration++) {
53
+ const errors = training.map(error);
54
+ const scale = Math.max(2, median(errors) * 2);
55
+ const matrix = Array.from({ length: 3 }, () => [0, 0, 0]);
56
+ const values = Array.from({ length: 3 }, () => [0, 0, 0]);
57
+ for (let index = 0; index < training.length; index++) {
58
+ const point = training[index];
59
+ const weight = Math.min(1, scale / Math.max(errors[index], 0.001)) ** 2;
60
+ for (let row = 0; row < 3; row++) {
61
+ for (let column = 0; column < 3; column++) matrix[row][column] += weight * point.vector[row] * point.vector[column];
62
+ for (let channel = 0; channel < 3; channel++) values[channel][row] += weight * point.vector[row] * point.rgb[channel];
63
+ }
64
+ }
65
+ coefficients = values.map(value => solvePlane(matrix, value));
66
+ if (coefficients.some(row => row === null)) return null;
67
+ }
68
+ const backgroundSupport = validation.filter(point => error(point) < 3).length / validation.length;
69
+ if (backgroundSupport < 0.8) return null;
70
+ const trials = [];
71
+ for (let step = 0; step <= 24; step++) {
72
+ const gain = step * 0.05;
73
+ let matches = 0;
74
+ let total = 0;
75
+ let inlierError = 0;
76
+ const quadrants = Array.from({ length: 4 }, () => ({ matches: 0, total: 0 }));
77
+ for (let y = 0; y < size; y++) {
78
+ for (let x = 0; x < size; x++) {
79
+ const alpha = alphaMap[y * size + x];
80
+ if (alpha < 0.1) continue;
81
+ const background = predict([1, x / size, y / size]);
82
+ if (Math.max(...background) > 245 || Math.min(...background) < 0) continue;
83
+ const difference = read(x, y).reduce((sum, value, channel) => sum + Math.abs(
84
+ value - ((1 - alpha * gain) * background[channel] + alpha * gain * 255)
85
+ ), 0) / 3;
86
+ const quadrant = quadrants[(y >= size / 2 ? 2 : 0) + (x >= size / 2 ? 1 : 0)];
87
+ total++;
88
+ quadrant.total++;
89
+ if (difference < 3) {
90
+ matches++;
91
+ quadrant.matches++;
92
+ inlierError += difference;
93
+ }
94
+ }
95
+ }
96
+ trials.push({ gain, fraction: matches / total, error: matches ? inlierError / matches : Infinity,
97
+ quadrants: quadrants.map(quadrant => quadrant.matches / quadrant.total) });
98
+ }
99
+ trials.sort((left, right) => right.fraction - left.fraction || left.error - right.error);
100
+ const best = trials[0];
101
+ const zero = trials.find(trial => trial.gain === 0);
102
+ if (best.gain !== 1 || best.fraction < 0.55 || zero.fraction >= 0.1 ||
103
+ !best.quadrants.every(fraction => fraction >= 0.3)) return null;
104
+ return { backgroundSupport, gain: best.gain, matchFraction: best.fraction,
105
+ quadrantSupport: best.quadrants, zeroGainSupport: zero.fraction };
106
+ }
@@ -0,0 +1,59 @@
1
+ // Estimate a white watermark's gain from neighboring source pixels. This is
2
+ // evidence for inverse-alpha candidates only; never synthesize replacement rows.
3
+ function quantile(values, fraction) {
4
+ if (values.length === 0) return NaN;
5
+ const sorted = values.slice().sort((a, b) => a - b);
6
+ return sorted[Math.floor((sorted.length - 1) * fraction)];
7
+ }
8
+
9
+ export function measureRowAlphaEvidence(image, position, alphaMap) {
10
+ if (!image?.data || !position || position.width !== 48 || position.height !== 48 ||
11
+ alphaMap?.length !== 48 * 48 || position.x < 20 || position.y < 0 ||
12
+ position.x + 68 > image.width || position.y + 48 > image.height) return null;
13
+ const gains = [];
14
+ const quadrants = [[], [], [], []];
15
+ const points = [];
16
+ let outsideError = 0;
17
+ let outsideCount = 0;
18
+ for (let row = 0; row < 48; row++) {
19
+ for (let channel = 0; channel < 3; channel++) {
20
+ const value = column => image.data[
21
+ ((position.y + row) * image.width + position.x + column) * 4 + channel
22
+ ];
23
+ const left = quantile(Array.from({ length: 12 }, (_, i) => value(-20 + i)), 0.5);
24
+ const right = quantile(Array.from({ length: 12 }, (_, i) => value(56 + i)), 0.5);
25
+ const background = column => left + (right - left) * (column + 14.5) / 76;
26
+ for (const column of [-8, -6, -4, -2, 48, 50, 52, 54]) {
27
+ outsideError += Math.abs(value(column) - background(column));
28
+ outsideCount++;
29
+ }
30
+ for (let column = 0; column < 48; column++) {
31
+ const alpha = alphaMap[row * 48 + column];
32
+ const reference = background(column);
33
+ if (alpha < 0.05 || reference > 220) continue;
34
+ const observed = value(column);
35
+ const gain = (observed - reference) / ((255 - reference) * alpha);
36
+ gains.push(gain);
37
+ quadrants[(row >= 24 ? 2 : 0) + (column >= 24 ? 1 : 0)].push(gain);
38
+ points.push({ observed, reference, alpha });
39
+ }
40
+ }
41
+ }
42
+ if (quadrants.some(values => values.length < 24)) return null;
43
+ const gain = quantile(gains, 0.5);
44
+ return {
45
+ gain,
46
+ spread: quantile(gains, 0.75) - quantile(gains, 0.25),
47
+ quadrants: quadrants.map(values => quantile(values, 0.5)),
48
+ outsideError: outsideError / outsideCount,
49
+ fitError: points.reduce((sum, point) => sum + Math.abs(
50
+ point.observed - point.reference - (255 - point.reference) * point.alpha * gain
51
+ ), 0) / points.length
52
+ };
53
+ }
54
+
55
+ export function supportsRowAlphaGain(evidence, gain = 0.6) {
56
+ return evidence != null && Math.abs(evidence.gain - gain) <= 0.05 &&
57
+ evidence.spread <= 0.08 && evidence.outsideError <= 4 && evidence.fitError <= 3 &&
58
+ evidence.quadrants.every(value => Math.abs(value - gain) <= 0.08);
59
+ }