@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
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|