@pilio/gemini-watermark-remover 1.0.20 → 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.
- package/package.json +67 -2
- package/src/cli/gwrCli.js +1 -1
- package/src/cli/gwrRemoveCommand.js +82 -14
- package/src/core/adaptiveDetector.js +22 -3
- package/src/core/allenkFdncnnDenoise.js +326 -0
- package/src/core/allenkFdncnnNcnnModel.js +244 -0
- package/src/core/allenkFdncnnOnnxExport.js +337 -0
- package/src/core/allenkFdncnnOnnxRuntime.js +138 -0
- package/src/core/allenkFdncnnReferenceRuntime.js +169 -0
- package/src/core/alphaGradientMask.js +165 -0
- package/src/core/blendModes.js +9 -4
- package/src/core/candidateSelector.js +422 -33
- package/src/core/embeddedAlphaMaps.js +2 -0
- package/src/core/geminiSizeCatalog.js +44 -1
- package/src/core/restorationMetrics.js +44 -0
- package/src/core/watermarkEngine.js +2 -1
- package/src/core/watermarkProcessor.js +1048 -82
- package/src/sdk/index.d.ts +29 -1
- package/src/sdk/node.d.ts +13 -0
- package/src/sdk/node.js +7 -0
- package/src/sdk/video.d.ts +84 -0
- package/src/sdk/video.js +263 -0
- package/src/shared/debugFileHandoff.js +109 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAllenkFdncnnInput,
|
|
3
|
+
convertAllenkFdncnnOutputToRgba
|
|
4
|
+
} from './allenkFdncnnDenoise.js';
|
|
5
|
+
import { halfToFloat } from './allenkFdncnnNcnnModel.js';
|
|
6
|
+
|
|
7
|
+
function clamp(value, min, max) {
|
|
8
|
+
return Math.max(min, Math.min(max, value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readUInt16LE(bytes, offset) {
|
|
12
|
+
return bytes[offset] | (bytes[offset + 1] << 8);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readFloat32LE(bytes, offset) {
|
|
16
|
+
return new DataView(bytes.buffer, bytes.byteOffset + offset, 4).getFloat32(0, true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function calculateSegmentMacs(segment, width, height) {
|
|
20
|
+
return width * height *
|
|
21
|
+
segment.outputChannels *
|
|
22
|
+
segment.inputChannels *
|
|
23
|
+
segment.kernelW *
|
|
24
|
+
segment.kernelH;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateLayoutMacs(segments, width, height) {
|
|
28
|
+
return segments.reduce((sum, segment) => sum + calculateSegmentMacs(segment, width, height), 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function decodeSegmentWeights(segment, weightBin) {
|
|
32
|
+
const weights = new Float32Array(segment.weightCount);
|
|
33
|
+
for (let i = 0; i < segment.weightCount; i++) {
|
|
34
|
+
weights[i] = halfToFloat(readUInt16LE(weightBin, segment.weightOffset + i * 2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bias = new Float32Array(segment.biasCount);
|
|
38
|
+
for (let i = 0; i < segment.biasCount; i++) {
|
|
39
|
+
bias[i] = readFloat32LE(weightBin, segment.biasOffset + i * 4);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { weights, bias };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runSamePaddingConvolution({
|
|
46
|
+
input,
|
|
47
|
+
width,
|
|
48
|
+
height,
|
|
49
|
+
segment,
|
|
50
|
+
weights,
|
|
51
|
+
bias
|
|
52
|
+
}) {
|
|
53
|
+
const outputChannels = segment.outputChannels;
|
|
54
|
+
const inputChannels = segment.inputChannels;
|
|
55
|
+
const output = new Float32Array(width * height * outputChannels);
|
|
56
|
+
const kernelW = segment.kernelW;
|
|
57
|
+
const kernelH = segment.kernelH;
|
|
58
|
+
const padX = segment.padW;
|
|
59
|
+
const padY = segment.padH;
|
|
60
|
+
const relu = segment.activationType === 1;
|
|
61
|
+
const plane = width * height;
|
|
62
|
+
|
|
63
|
+
for (let oc = 0; oc < outputChannels; oc++) {
|
|
64
|
+
const outputBase = oc * plane;
|
|
65
|
+
const biasValue = bias[oc] || 0;
|
|
66
|
+
for (let y = 0; y < height; y++) {
|
|
67
|
+
for (let x = 0; x < width; x++) {
|
|
68
|
+
let sum = biasValue;
|
|
69
|
+
for (let ic = 0; ic < inputChannels; ic++) {
|
|
70
|
+
const inputBase = ic * plane;
|
|
71
|
+
const weightBase = (((oc * inputChannels) + ic) * kernelH) * kernelW;
|
|
72
|
+
for (let ky = 0; ky < kernelH; ky++) {
|
|
73
|
+
const sy = y + ky - padY;
|
|
74
|
+
if (sy < 0 || sy >= height) continue;
|
|
75
|
+
for (let kx = 0; kx < kernelW; kx++) {
|
|
76
|
+
const sx = x + kx - padX;
|
|
77
|
+
if (sx < 0 || sx >= width) continue;
|
|
78
|
+
sum += input[inputBase + sy * width + sx] * weights[weightBase + ky * kernelW + kx];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
output[outputBase + y * width + x] = relu ? Math.max(0, sum) : sum;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createAllenkFdncnnReferenceRuntime({
|
|
91
|
+
weightBin,
|
|
92
|
+
weightLayout,
|
|
93
|
+
maxMacs = 50_000_000
|
|
94
|
+
} = {}) {
|
|
95
|
+
if (!weightBin || !weightLayout?.segments?.length) {
|
|
96
|
+
throw new Error('Missing allenk FDnCNN weight bin or layout');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const bin = weightBin instanceof Uint8Array ? weightBin : new Uint8Array(weightBin);
|
|
100
|
+
const decodedSegments = weightLayout.segments.map((segment) => ({
|
|
101
|
+
segment,
|
|
102
|
+
...decodeSegmentWeights(segment, bin)
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: 'allenk-fdncnn-pure-js-reference',
|
|
107
|
+
status: 'debug-only',
|
|
108
|
+
maxMacs,
|
|
109
|
+
estimateMacs(width, height) {
|
|
110
|
+
return calculateLayoutMacs(weightLayout.segments, width, height);
|
|
111
|
+
},
|
|
112
|
+
execute(input, width, height) {
|
|
113
|
+
const macs = calculateLayoutMacs(weightLayout.segments, width, height);
|
|
114
|
+
if (macs > maxMacs) {
|
|
115
|
+
const error = new Error(`allenk FDnCNN pure JS reference refused ${macs} MACs; max=${maxMacs}`);
|
|
116
|
+
error.code = 'ALLENK_FDNCNN_REFERENCE_MAC_LIMIT';
|
|
117
|
+
error.macs = macs;
|
|
118
|
+
error.maxMacs = maxMacs;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let current = input;
|
|
123
|
+
for (const decoded of decodedSegments) {
|
|
124
|
+
current = runSamePaddingConvolution({
|
|
125
|
+
input: current,
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
segment: decoded.segment,
|
|
129
|
+
weights: decoded.weights,
|
|
130
|
+
bias: decoded.bias
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < current.length; i++) {
|
|
135
|
+
current[i] = clamp(current[i], 0, 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
output: current,
|
|
140
|
+
macs,
|
|
141
|
+
runtime: this.id
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
denoiseImageData({ imageData, sigma } = {}) {
|
|
145
|
+
const input = buildAllenkFdncnnInput({ imageData, sigma });
|
|
146
|
+
const result = this.execute(input, imageData.width, imageData.height);
|
|
147
|
+
return {
|
|
148
|
+
...result,
|
|
149
|
+
imageData: {
|
|
150
|
+
width: imageData.width,
|
|
151
|
+
height: imageData.height,
|
|
152
|
+
data: convertAllenkFdncnnOutputToRgba({
|
|
153
|
+
output: result.output,
|
|
154
|
+
width: imageData.width,
|
|
155
|
+
height: imageData.height
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
calculateLayoutMacs,
|
|
165
|
+
calculateSegmentMacs,
|
|
166
|
+
createAllenkFdncnnReferenceRuntime,
|
|
167
|
+
decodeSegmentWeights,
|
|
168
|
+
runSamePaddingConvolution
|
|
169
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const EPSILON = 1e-8;
|
|
2
|
+
|
|
3
|
+
function clamp(value, min, max) {
|
|
4
|
+
return Math.max(min, Math.min(max, value));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function gaussianKernel1D(sigma) {
|
|
8
|
+
if (!Number.isFinite(sigma) || sigma <= 0) return null;
|
|
9
|
+
|
|
10
|
+
const radius = Math.max(1, Math.ceil(sigma * 3));
|
|
11
|
+
const kernel = new Float32Array(radius * 2 + 1);
|
|
12
|
+
let sum = 0;
|
|
13
|
+
|
|
14
|
+
for (let i = -radius; i <= radius; i++) {
|
|
15
|
+
const value = Math.exp(-(i * i) / (2 * sigma * sigma));
|
|
16
|
+
kernel[i + radius] = value;
|
|
17
|
+
sum += value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (sum <= EPSILON) return null;
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < kernel.length; i++) {
|
|
23
|
+
kernel[i] /= sum;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { kernel, radius };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function blurHorizontal(values, width, height, kernelInfo) {
|
|
30
|
+
const out = new Float32Array(values.length);
|
|
31
|
+
const { kernel, radius } = kernelInfo;
|
|
32
|
+
|
|
33
|
+
for (let y = 0; y < height; y++) {
|
|
34
|
+
const rowBase = y * width;
|
|
35
|
+
for (let x = 0; x < width; x++) {
|
|
36
|
+
let sum = 0;
|
|
37
|
+
for (let k = -radius; k <= radius; k++) {
|
|
38
|
+
const sx = clamp(x + k, 0, width - 1);
|
|
39
|
+
sum += values[rowBase + sx] * kernel[k + radius];
|
|
40
|
+
}
|
|
41
|
+
out[rowBase + x] = sum;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function blurVertical(values, width, height, kernelInfo) {
|
|
49
|
+
const out = new Float32Array(values.length);
|
|
50
|
+
const { kernel, radius } = kernelInfo;
|
|
51
|
+
|
|
52
|
+
for (let y = 0; y < height; y++) {
|
|
53
|
+
for (let x = 0; x < width; x++) {
|
|
54
|
+
let sum = 0;
|
|
55
|
+
for (let k = -radius; k <= radius; k++) {
|
|
56
|
+
const sy = clamp(y + k, 0, height - 1);
|
|
57
|
+
sum += values[sy * width + x] * kernel[k + radius];
|
|
58
|
+
}
|
|
59
|
+
out[y * width + x] = sum;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function gaussianBlur(values, width, height, sigma) {
|
|
67
|
+
const kernelInfo = gaussianKernel1D(sigma);
|
|
68
|
+
if (!kernelInfo) return new Float32Array(values);
|
|
69
|
+
|
|
70
|
+
return blurVertical(
|
|
71
|
+
blurHorizontal(values, width, height, kernelInfo),
|
|
72
|
+
width,
|
|
73
|
+
height,
|
|
74
|
+
kernelInfo
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dilate(values, width, height, radius) {
|
|
79
|
+
if (!Number.isFinite(radius) || radius <= 0) return new Float32Array(values);
|
|
80
|
+
|
|
81
|
+
const roundedRadius = Math.max(1, Math.round(radius));
|
|
82
|
+
const radiusSquared = roundedRadius * roundedRadius;
|
|
83
|
+
const out = new Float32Array(values.length);
|
|
84
|
+
|
|
85
|
+
for (let y = 0; y < height; y++) {
|
|
86
|
+
for (let x = 0; x < width; x++) {
|
|
87
|
+
let maxValue = 0;
|
|
88
|
+
for (let dy = -roundedRadius; dy <= roundedRadius; dy++) {
|
|
89
|
+
for (let dx = -roundedRadius; dx <= roundedRadius; dx++) {
|
|
90
|
+
if (dx * dx + dy * dy > radiusSquared) continue;
|
|
91
|
+
const sx = x + dx;
|
|
92
|
+
const sy = y + dy;
|
|
93
|
+
if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
|
|
94
|
+
maxValue = Math.max(maxValue, values[sy * width + sx]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
out[y * width + x] = maxValue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createAlphaGradientMask({
|
|
105
|
+
alphaMap,
|
|
106
|
+
width,
|
|
107
|
+
height = width,
|
|
108
|
+
strength = 1,
|
|
109
|
+
gamma = 0.5,
|
|
110
|
+
dilateRadius = 2,
|
|
111
|
+
blurSigma = 2
|
|
112
|
+
}) {
|
|
113
|
+
if (!alphaMap || width <= 0 || height <= 0 || alphaMap.length < width * height) {
|
|
114
|
+
return new Float32Array(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const gradient = new Float32Array(width * height);
|
|
118
|
+
let minGradient = Number.POSITIVE_INFINITY;
|
|
119
|
+
let maxGradient = 0;
|
|
120
|
+
|
|
121
|
+
for (let y = 1; y < height - 1; y++) {
|
|
122
|
+
for (let x = 1; x < width - 1; x++) {
|
|
123
|
+
const i = y * width + x;
|
|
124
|
+
const gx =
|
|
125
|
+
-alphaMap[i - width - 1] - 2 * alphaMap[i - 1] - alphaMap[i + width - 1] +
|
|
126
|
+
alphaMap[i - width + 1] + 2 * alphaMap[i + 1] + alphaMap[i + width + 1];
|
|
127
|
+
const gy =
|
|
128
|
+
-alphaMap[i - width - 1] - 2 * alphaMap[i - width] - alphaMap[i - width + 1] +
|
|
129
|
+
alphaMap[i + width - 1] + 2 * alphaMap[i + width] + alphaMap[i + width + 1];
|
|
130
|
+
const value = Math.sqrt(gx * gx + gy * gy);
|
|
131
|
+
gradient[i] = value;
|
|
132
|
+
minGradient = Math.min(minGradient, value);
|
|
133
|
+
maxGradient = Math.max(maxGradient, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Number.isFinite(minGradient) || maxGradient <= minGradient + EPSILON) {
|
|
138
|
+
return new Float32Array(width * height);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const normalized = new Float32Array(width * height);
|
|
142
|
+
const exponent = Number.isFinite(gamma) && gamma > 0 ? gamma : 1;
|
|
143
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
144
|
+
const value = (gradient[i] - minGradient) / (maxGradient - minGradient);
|
|
145
|
+
normalized[i] = Math.pow(clamp(value, 0, 1), exponent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const expanded = dilate(normalized, width, height, dilateRadius);
|
|
149
|
+
const blurred = gaussianBlur(expanded, width, height, blurSigma);
|
|
150
|
+
const safeStrength = Number.isFinite(strength) ? Math.max(0, strength) : 1;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < blurred.length; i++) {
|
|
153
|
+
blurred[i] = clamp(blurred[i] * safeStrength, 0, 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return blurred;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getAlphaGradientWeight(mask, index, floor = 0.35) {
|
|
160
|
+
if (!mask || index < 0 || index >= mask.length) {
|
|
161
|
+
return clamp(floor, 0, 1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Math.max(clamp(floor, 0, 1), clamp(mask[index], 0, 1));
|
|
165
|
+
}
|
package/src/core/blendModes.js
CHANGED
|
@@ -37,11 +37,16 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
|
|
|
37
37
|
// Calculate index in alpha map
|
|
38
38
|
const alphaIdx = row * width + col;
|
|
39
39
|
|
|
40
|
-
// Get alpha value
|
|
40
|
+
// Get alpha value. A negative alpha map marks a dark-polarity
|
|
41
|
+
// watermark: same opacity mask, black logo value.
|
|
41
42
|
const rawAlpha = alphaMap[alphaIdx];
|
|
43
|
+
const alphaMagnitude = Math.abs(rawAlpha);
|
|
44
|
+
const logoValue = Number.isFinite(options.logoValue)
|
|
45
|
+
? options.logoValue
|
|
46
|
+
: (rawAlpha < 0 ? 0 : LOGO_VALUE);
|
|
42
47
|
|
|
43
48
|
// Remove low-level alpha noise from compressed background capture.
|
|
44
|
-
const signalAlpha = Math.max(0,
|
|
49
|
+
const signalAlpha = Math.max(0, alphaMagnitude - ALPHA_NOISE_FLOOR) * alphaGain;
|
|
45
50
|
|
|
46
51
|
// Skip very small alpha values (noise)
|
|
47
52
|
if (signalAlpha < ALPHA_THRESHOLD) {
|
|
@@ -49,7 +54,7 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
// Use original alpha for inverse solve; use denoised alpha as activation signal.
|
|
52
|
-
const alpha = Math.min(
|
|
57
|
+
const alpha = Math.min(alphaMagnitude * alphaGain, MAX_ALPHA);
|
|
53
58
|
const oneMinusAlpha = 1.0 - alpha;
|
|
54
59
|
|
|
55
60
|
// Apply reverse alpha blending to each RGB channel
|
|
@@ -57,7 +62,7 @@ export function removeWatermark(imageData, alphaMap, position, options = {}) {
|
|
|
57
62
|
const watermarked = imageData.data[imgIdx + c];
|
|
58
63
|
|
|
59
64
|
// Reverse alpha blending formula
|
|
60
|
-
const original = (watermarked - alpha *
|
|
65
|
+
const original = (watermarked - alpha * logoValue) / oneMinusAlpha;
|
|
61
66
|
|
|
62
67
|
// Clip to [0, 255] range
|
|
63
68
|
imageData.data[imgIdx + c] = Math.max(0, Math.min(255, Math.round(original)));
|