@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.
@@ -0,0 +1,326 @@
1
+ import { createAlphaGradientMask } from './alphaGradientMask.js';
2
+
3
+ const ALLENK_FDNCNN_MODEL = Object.freeze({
4
+ name: 'FDnCNN Color FP16',
5
+ upstream: 'allenk/GeminiWatermarkTool',
6
+ license: 'MIT',
7
+ runtime: 'NCNN',
8
+ inputBlob: 0,
9
+ outputBlob: 20,
10
+ inputLayout: '[R, G, B, sigma] CHW float32',
11
+ outputLayout: '[R, G, B] CHW float32',
12
+ defaultSigma: 25,
13
+ defaultStrength: 0.85,
14
+ defaultPadding: 16,
15
+ maxSigma: 150,
16
+ maxStrength: 3
17
+ });
18
+
19
+ function clamp(value, min, max) {
20
+ return Math.max(min, Math.min(max, value));
21
+ }
22
+
23
+ function createGaussianKernel(sigma, radius = Math.ceil(sigma * 3)) {
24
+ const safeSigma = Math.max(0.01, sigma);
25
+ const safeRadius = Math.max(1, Math.round(radius));
26
+ const kernel = new Float32Array(safeRadius * 2 + 1);
27
+ let sum = 0;
28
+
29
+ for (let i = -safeRadius; i <= safeRadius; i++) {
30
+ const value = Math.exp(-(i * i) / (2 * safeSigma * safeSigma));
31
+ kernel[i + safeRadius] = value;
32
+ sum += value;
33
+ }
34
+
35
+ for (let i = 0; i < kernel.length; i++) {
36
+ kernel[i] /= sum;
37
+ }
38
+
39
+ return { kernel, radius: safeRadius };
40
+ }
41
+
42
+ function gaussianBlurFloatMap(source, width, height, sigma, radius = Math.ceil(sigma * 3)) {
43
+ if (!source || width <= 0 || height <= 0 || !Number.isFinite(sigma) || sigma <= 0) {
44
+ return new Float32Array(source || 0);
45
+ }
46
+
47
+ const { kernel, radius: r } = createGaussianKernel(sigma, radius);
48
+ const temp = new Float32Array(source.length);
49
+ const output = new Float32Array(source.length);
50
+
51
+ for (let y = 0; y < height; y++) {
52
+ for (let x = 0; x < width; x++) {
53
+ let sum = 0;
54
+ for (let dx = -r; dx <= r; dx++) {
55
+ const xx = clamp(x + dx, 0, width - 1);
56
+ sum += source[y * width + xx] * kernel[dx + r];
57
+ }
58
+ temp[y * width + x] = sum;
59
+ }
60
+ }
61
+
62
+ for (let y = 0; y < height; y++) {
63
+ for (let x = 0; x < width; x++) {
64
+ let sum = 0;
65
+ for (let dy = -r; dy <= r; dy++) {
66
+ const yy = clamp(y + dy, 0, height - 1);
67
+ sum += temp[yy * width + x] * kernel[dy + r];
68
+ }
69
+ output[y * width + x] = sum;
70
+ }
71
+ }
72
+
73
+ return output;
74
+ }
75
+
76
+ function inferSquareSize(alphaMap) {
77
+ const size = Math.round(Math.sqrt(alphaMap?.length || 0));
78
+ return size > 0 && size * size === alphaMap.length ? size : 0;
79
+ }
80
+
81
+ function resizeSquareAlphaMapArea(sourceAlpha, sourceSize, targetWidth, targetHeight = targetWidth) {
82
+ if (!sourceAlpha || sourceSize <= 0 || targetWidth <= 0 || targetHeight <= 0) {
83
+ return new Float32Array(0);
84
+ }
85
+ if (sourceSize === targetWidth && sourceSize === targetHeight) {
86
+ return new Float32Array(sourceAlpha);
87
+ }
88
+
89
+ const output = new Float32Array(targetWidth * targetHeight);
90
+ const scaleX = sourceSize / targetWidth;
91
+ const scaleY = sourceSize / targetHeight;
92
+
93
+ for (let y = 0; y < targetHeight; y++) {
94
+ const yStart = y * scaleY;
95
+ const yEnd = (y + 1) * scaleY;
96
+ const y0 = Math.floor(yStart);
97
+ const y1 = Math.ceil(yEnd);
98
+
99
+ for (let x = 0; x < targetWidth; x++) {
100
+ const xStart = x * scaleX;
101
+ const xEnd = (x + 1) * scaleX;
102
+ const x0 = Math.floor(xStart);
103
+ const x1 = Math.ceil(xEnd);
104
+
105
+ let sum = 0;
106
+ let areaSum = 0;
107
+ for (let sy = y0; sy < y1; sy++) {
108
+ if (sy < 0 || sy >= sourceSize) continue;
109
+ const wy = Math.max(0, Math.min(yEnd, sy + 1) - Math.max(yStart, sy));
110
+ for (let sx = x0; sx < x1; sx++) {
111
+ if (sx < 0 || sx >= sourceSize) continue;
112
+ const wx = Math.max(0, Math.min(xEnd, sx + 1) - Math.max(xStart, sx));
113
+ const area = wx * wy;
114
+ sum += sourceAlpha[sy * sourceSize + sx] * area;
115
+ areaSum += area;
116
+ }
117
+ }
118
+
119
+ output[y * targetWidth + x] = areaSum > 0 ? sum / areaSum : 0;
120
+ }
121
+ }
122
+
123
+ return output;
124
+ }
125
+
126
+ function normalizeAllenkFdncnnOptions(options = {}) {
127
+ const sigma = Number.isFinite(options.sigma)
128
+ ? clamp(options.sigma, 0, ALLENK_FDNCNN_MODEL.maxSigma)
129
+ : ALLENK_FDNCNN_MODEL.defaultSigma;
130
+ const strength = Number.isFinite(options.strength)
131
+ ? clamp(options.strength, 0, ALLENK_FDNCNN_MODEL.maxStrength)
132
+ : ALLENK_FDNCNN_MODEL.defaultStrength;
133
+ const padding = Number.isFinite(options.padding)
134
+ ? Math.max(0, Math.round(options.padding))
135
+ : ALLENK_FDNCNN_MODEL.defaultPadding;
136
+
137
+ return { sigma, strength, padding };
138
+ }
139
+
140
+ function createAllenkGradientMask({
141
+ alphaMap,
142
+ width,
143
+ height = width,
144
+ strength = ALLENK_FDNCNN_MODEL.defaultStrength
145
+ } = {}) {
146
+ const sourceSize = inferSquareSize(alphaMap);
147
+ const resizedAlphaMap = sourceSize > 0
148
+ ? resizeSquareAlphaMapArea(alphaMap, sourceSize, width, height)
149
+ : alphaMap;
150
+
151
+ return createAlphaGradientMask({
152
+ alphaMap: resizedAlphaMap,
153
+ width,
154
+ height,
155
+ strength,
156
+ gamma: 0.5,
157
+ dilateRadius: 2,
158
+ blurSigma: 2
159
+ });
160
+ }
161
+
162
+ function calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding = ALLENK_FDNCNN_MODEL.defaultPadding } = {}) {
163
+ if (!region || imageWidth <= 0 || imageHeight <= 0 || region.width <= 0 || region.height <= 0) {
164
+ return null;
165
+ }
166
+
167
+ const safePadding = Math.max(0, Math.round(padding));
168
+ const x = clamp(Math.round(region.x - safePadding), 0, imageWidth);
169
+ const y = clamp(Math.round(region.y - safePadding), 0, imageHeight);
170
+ const right = clamp(Math.round(region.x + region.width + safePadding), 0, imageWidth);
171
+ const bottom = clamp(Math.round(region.y + region.height + safePadding), 0, imageHeight);
172
+ const width = right - x;
173
+ const height = bottom - y;
174
+
175
+ if (width < 4 || height < 4) return null;
176
+
177
+ return {
178
+ x,
179
+ y,
180
+ width,
181
+ height,
182
+ inner: {
183
+ x: clamp(Math.round(region.x - x), 0, width),
184
+ y: clamp(Math.round(region.y - y), 0, height),
185
+ width: clamp(Math.round(region.width), 0, width),
186
+ height: clamp(Math.round(region.height), 0, height)
187
+ }
188
+ };
189
+ }
190
+
191
+ function embedAllenkRoiWeights({ roiWeights, roiWidth, roiHeight, paddedRoi, blurSigma = 1 } = {}) {
192
+ if (!roiWeights || !paddedRoi?.inner || roiWidth <= 0 || roiHeight <= 0) {
193
+ return new Float32Array(0);
194
+ }
195
+
196
+ const weights = new Float32Array(paddedRoi.width * paddedRoi.height);
197
+ const inner = paddedRoi.inner;
198
+
199
+ for (let y = 0; y < roiHeight; y++) {
200
+ const py = inner.y + y;
201
+ if (py < 0 || py >= paddedRoi.height) continue;
202
+
203
+ for (let x = 0; x < roiWidth; x++) {
204
+ const px = inner.x + x;
205
+ if (px < 0 || px >= paddedRoi.width) continue;
206
+ weights[py * paddedRoi.width + px] = clamp(roiWeights[y * roiWidth + x] || 0, 0, 1);
207
+ }
208
+ }
209
+
210
+ return gaussianBlurFloatMap(weights, paddedRoi.width, paddedRoi.height, blurSigma);
211
+ }
212
+
213
+ function buildAllenkFdncnnInput({ imageData, sigma = ALLENK_FDNCNN_MODEL.defaultSigma } = {}) {
214
+ if (!imageData?.data || imageData.width <= 0 || imageData.height <= 0) {
215
+ return new Float32Array(0);
216
+ }
217
+
218
+ const { width, height, data } = imageData;
219
+ const pixelCount = width * height;
220
+ const input = new Float32Array(pixelCount * 4);
221
+ const sigmaNorm = clamp(sigma, 0, ALLENK_FDNCNN_MODEL.maxSigma) / 255;
222
+ const stride = data.length >= pixelCount * 4 ? 4 : 3;
223
+
224
+ for (let i = 0; i < pixelCount; i++) {
225
+ const src = i * stride;
226
+ input[i] = (data[src] || 0) / 255;
227
+ input[pixelCount + i] = (data[src + 1] || 0) / 255;
228
+ input[pixelCount * 2 + i] = (data[src + 2] || 0) / 255;
229
+ input[pixelCount * 3 + i] = sigmaNorm;
230
+ }
231
+
232
+ return input;
233
+ }
234
+
235
+ function convertAllenkFdncnnOutputToRgba({ output, width, height, alpha = 255 } = {}) {
236
+ if (!output || width <= 0 || height <= 0) {
237
+ return new Uint8ClampedArray(0);
238
+ }
239
+
240
+ const pixelCount = width * height;
241
+ const rgba = new Uint8ClampedArray(pixelCount * 4);
242
+
243
+ for (let i = 0; i < pixelCount; i++) {
244
+ rgba[i * 4] = Math.round(clamp(output[i] || 0, 0, 1) * 255);
245
+ rgba[i * 4 + 1] = Math.round(clamp(output[pixelCount + i] || 0, 0, 1) * 255);
246
+ rgba[i * 4 + 2] = Math.round(clamp(output[pixelCount * 2 + i] || 0, 0, 1) * 255);
247
+ rgba[i * 4 + 3] = alpha;
248
+ }
249
+
250
+ return rgba;
251
+ }
252
+
253
+ function getLocalMeanRgb(data, width, height, x, y, channel) {
254
+ let sum = 0;
255
+ let count = 0;
256
+ for (let dy = -1; dy <= 1; dy++) {
257
+ const yy = y + dy;
258
+ if (yy < 0 || yy >= height) continue;
259
+ for (let dx = -1; dx <= 1; dx++) {
260
+ const xx = x + dx;
261
+ if (xx < 0 || xx >= width) continue;
262
+ sum += data[(yy * width + xx) * 4 + channel];
263
+ count++;
264
+ }
265
+ }
266
+ return count > 0 ? sum / count : data[(y * width + x) * 4 + channel];
267
+ }
268
+
269
+ function blendAllenkDenoisedRoi({
270
+ originalData,
271
+ denoisedData,
272
+ weights,
273
+ width = 0,
274
+ height = 0,
275
+ preserveHighpassStrength = 0
276
+ } = {}) {
277
+ if (!originalData || !denoisedData || !weights || originalData.length !== denoisedData.length) {
278
+ return new Uint8ClampedArray(originalData || 0);
279
+ }
280
+
281
+ const output = new Uint8ClampedArray(originalData);
282
+ const pixelCount = Math.min(weights.length, Math.floor(originalData.length / 4));
283
+ const canPreserveHighpass = (
284
+ Number.isFinite(preserveHighpassStrength) &&
285
+ preserveHighpassStrength > 0 &&
286
+ Number.isFinite(width) &&
287
+ Number.isFinite(height) &&
288
+ width > 0 &&
289
+ height > 0 &&
290
+ width * height <= pixelCount
291
+ );
292
+
293
+ for (let pixel = 0; pixel < pixelCount; pixel++) {
294
+ const weight = clamp(weights[pixel] || 0, 0, 1);
295
+ if (weight <= 0) continue;
296
+
297
+ const idx = pixel * 4;
298
+ const x = canPreserveHighpass ? pixel % width : 0;
299
+ const y = canPreserveHighpass ? Math.floor(pixel / width) : 0;
300
+ const highpassGain = canPreserveHighpass
301
+ ? Math.min(0.28, weight * preserveHighpassStrength)
302
+ : 0;
303
+ for (let c = 0; c < 3; c++) {
304
+ const blended = (
305
+ originalData[idx + c] * (1 - weight) + denoisedData[idx + c] * weight
306
+ );
307
+ const highpass = highpassGain > 0
308
+ ? clamp(originalData[idx + c] - getLocalMeanRgb(originalData, width, height, x, y, c), -14, 14)
309
+ : 0;
310
+ output[idx + c] = Math.round(clamp(blended + highpass * highpassGain, 0, 255));
311
+ }
312
+ }
313
+
314
+ return output;
315
+ }
316
+
317
+ export {
318
+ ALLENK_FDNCNN_MODEL,
319
+ blendAllenkDenoisedRoi,
320
+ buildAllenkFdncnnInput,
321
+ calculateAllenkPaddedRoi,
322
+ convertAllenkFdncnnOutputToRgba,
323
+ createAllenkGradientMask,
324
+ embedAllenkRoiWeights,
325
+ normalizeAllenkFdncnnOptions
326
+ };
@@ -0,0 +1,244 @@
1
+ const NCNN_BINARY_PARAM_MAGIC = 7767517;
2
+ const NCNN_LAYER_TYPES = Object.freeze({
3
+ 6: 'Convolution',
4
+ 16: 'Input'
5
+ });
6
+ const NCNN_WEIGHT_FP16_STORAGE_TAG = 0x01306b47;
7
+
8
+ function readInt32LE(buffer, offset) {
9
+ if (offset + 4 > buffer.length) {
10
+ throw new Error(`Unexpected end of NCNN param at byte ${offset}`);
11
+ }
12
+ return buffer.readInt32LE
13
+ ? buffer.readInt32LE(offset)
14
+ : new DataView(buffer.buffer, buffer.byteOffset + offset, 4).getInt32(0, true);
15
+ }
16
+
17
+ function readUInt32LE(buffer, offset) {
18
+ if (offset + 4 > buffer.length) {
19
+ throw new Error(`Unexpected end of NCNN bin at byte ${offset}`);
20
+ }
21
+ return buffer.readUInt32LE
22
+ ? buffer.readUInt32LE(offset)
23
+ : new DataView(buffer.buffer, buffer.byteOffset + offset, 4).getUint32(0, true);
24
+ }
25
+
26
+ function normalizeBuffer(buffer) {
27
+ if (buffer instanceof Uint8Array) return buffer;
28
+ return new Uint8Array(buffer);
29
+ }
30
+
31
+ function parseParamPairs(buffer, cursor) {
32
+ const params = {};
33
+ let offset = cursor;
34
+
35
+ while (offset < buffer.length) {
36
+ const key = readInt32LE(buffer, offset);
37
+ offset += 4;
38
+ if (key === -233) {
39
+ return { params, offset };
40
+ }
41
+
42
+ const value = readInt32LE(buffer, offset);
43
+ offset += 4;
44
+ params[key] = value;
45
+ }
46
+
47
+ throw new Error('NCNN layer params were not terminated by -233');
48
+ }
49
+
50
+ function getNcnnLayerTypeName(typeIndex) {
51
+ return NCNN_LAYER_TYPES[typeIndex] || `LayerType${typeIndex}`;
52
+ }
53
+
54
+ function normalizeConvolutionParams(params = {}) {
55
+ return {
56
+ numOutput: params[0] ?? 0,
57
+ kernelW: params[1] ?? 0,
58
+ dilationW: params[2] ?? 1,
59
+ strideW: params[3] ?? 1,
60
+ padW: params[4] ?? 0,
61
+ biasTerm: params[5] ?? 0,
62
+ weightDataSize: params[6] ?? 0,
63
+ activationType: params[9] ?? 0,
64
+ kernelH: params[11] ?? params[1] ?? 0,
65
+ dilationH: params[12] ?? params[2] ?? 1,
66
+ strideH: params[13] ?? params[3] ?? 1,
67
+ padH: params[14] ?? params[4] ?? 0
68
+ };
69
+ }
70
+
71
+ function parseAllenkFdncnnParam(bufferLike) {
72
+ const buffer = normalizeBuffer(bufferLike);
73
+ let offset = 0;
74
+
75
+ const magic = readInt32LE(buffer, offset);
76
+ offset += 4;
77
+ if (magic !== NCNN_BINARY_PARAM_MAGIC) {
78
+ throw new Error(`Unsupported NCNN binary param magic: ${magic}`);
79
+ }
80
+
81
+ const layerCount = readInt32LE(buffer, offset);
82
+ offset += 4;
83
+ const blobCount = readInt32LE(buffer, offset);
84
+ offset += 4;
85
+ const layers = [];
86
+
87
+ for (let i = 0; i < layerCount; i++) {
88
+ const typeIndex = readInt32LE(buffer, offset);
89
+ offset += 4;
90
+ const bottomCount = readInt32LE(buffer, offset);
91
+ offset += 4;
92
+ const topCount = readInt32LE(buffer, offset);
93
+ offset += 4;
94
+ const bottoms = [];
95
+ const tops = [];
96
+
97
+ for (let b = 0; b < bottomCount; b++) {
98
+ bottoms.push(readInt32LE(buffer, offset));
99
+ offset += 4;
100
+ }
101
+ for (let t = 0; t < topCount; t++) {
102
+ tops.push(readInt32LE(buffer, offset));
103
+ offset += 4;
104
+ }
105
+
106
+ const parsed = parseParamPairs(buffer, offset);
107
+ offset = parsed.offset;
108
+
109
+ const type = getNcnnLayerTypeName(typeIndex);
110
+ const layer = {
111
+ index: i,
112
+ typeIndex,
113
+ type,
114
+ bottoms,
115
+ tops,
116
+ params: parsed.params
117
+ };
118
+ if (type === 'Convolution') {
119
+ layer.convolution = normalizeConvolutionParams(parsed.params);
120
+ }
121
+ layers.push(layer);
122
+ }
123
+
124
+ return {
125
+ magic,
126
+ layerCount,
127
+ blobCount,
128
+ layers,
129
+ bytesRead: offset,
130
+ byteLength: buffer.length
131
+ };
132
+ }
133
+
134
+ function halfToFloat(half) {
135
+ const sign = (half & 0x8000) ? -1 : 1;
136
+ const exponent = (half >> 10) & 0x1f;
137
+ const fraction = half & 0x03ff;
138
+
139
+ if (exponent === 0) {
140
+ return sign * Math.pow(2, -14) * (fraction / 1024);
141
+ }
142
+ if (exponent === 0x1f) {
143
+ return fraction ? Number.NaN : sign * Number.POSITIVE_INFINITY;
144
+ }
145
+ return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
146
+ }
147
+
148
+ function buildAllenkFdncnnWeightLayout(param, binLike) {
149
+ const bin = normalizeBuffer(binLike);
150
+ let offset = 0;
151
+ let inputChannels = 4;
152
+ const segments = [];
153
+
154
+ for (const layer of param.layers) {
155
+ if (layer.type !== 'Convolution') {
156
+ continue;
157
+ }
158
+
159
+ const conv = layer.convolution;
160
+ const expectedWeightCount = conv.numOutput * inputChannels * conv.kernelW * conv.kernelH;
161
+ if (expectedWeightCount !== conv.weightDataSize) {
162
+ throw new Error(
163
+ `Unexpected weight count for layer ${layer.index}: expected ${expectedWeightCount}, got ${conv.weightDataSize}`
164
+ );
165
+ }
166
+
167
+ const storageTag = readUInt32LE(bin, offset);
168
+ if (storageTag !== NCNN_WEIGHT_FP16_STORAGE_TAG) {
169
+ throw new Error(`Unexpected NCNN weight storage tag at layer ${layer.index}: 0x${storageTag.toString(16)}`);
170
+ }
171
+ const weightOffset = offset + 4;
172
+ const weightBytes = conv.weightDataSize * 2;
173
+ const biasOffset = weightOffset + weightBytes;
174
+ const biasCount = conv.biasTerm ? conv.numOutput : 0;
175
+ const biasBytes = biasCount * 4;
176
+
177
+ if (biasOffset + biasBytes > bin.length) {
178
+ throw new Error(`NCNN weights exceed bin length at layer ${layer.index}`);
179
+ }
180
+
181
+ segments.push({
182
+ layerIndex: layer.index,
183
+ type: layer.type,
184
+ inputChannels,
185
+ outputChannels: conv.numOutput,
186
+ kernelW: conv.kernelW,
187
+ kernelH: conv.kernelH,
188
+ strideW: conv.strideW,
189
+ strideH: conv.strideH,
190
+ padW: conv.padW,
191
+ padH: conv.padH,
192
+ activationType: conv.activationType,
193
+ storageTag,
194
+ weightOffset,
195
+ weightBytes,
196
+ weightCount: conv.weightDataSize,
197
+ biasOffset,
198
+ biasBytes,
199
+ biasCount
200
+ });
201
+
202
+ offset = biasOffset + biasBytes;
203
+ inputChannels = conv.numOutput;
204
+ }
205
+
206
+ return {
207
+ storage: 'fp16-weights-fp32-bias',
208
+ byteLength: bin.length,
209
+ bytesRead: offset,
210
+ segments
211
+ };
212
+ }
213
+
214
+ function summarizeAllenkFdncnnModel(param, weightLayout = null) {
215
+ const convolutionLayers = param.layers.filter((layer) => layer.type === 'Convolution');
216
+ const reluLayers = convolutionLayers.filter((layer) => layer.convolution?.activationType === 1);
217
+ const outputLayer = convolutionLayers[convolutionLayers.length - 1] || null;
218
+
219
+ return {
220
+ layerCount: param.layerCount,
221
+ blobCount: param.blobCount,
222
+ convolutionLayerCount: convolutionLayers.length,
223
+ reluConvolutionLayerCount: reluLayers.length,
224
+ inputBlob: 0,
225
+ outputBlob: outputLayer?.tops?.[0] ?? null,
226
+ inputChannels: weightLayout?.segments?.[0]?.inputChannels ?? 4,
227
+ hiddenChannels: convolutionLayers[0]?.convolution?.numOutput ?? null,
228
+ outputChannels: outputLayer?.convolution?.numOutput ?? null,
229
+ kernel: convolutionLayers[0]
230
+ ? `${convolutionLayers[0].convolution.kernelW}x${convolutionLayers[0].convolution.kernelH}`
231
+ : null,
232
+ bytesRead: weightLayout?.bytesRead ?? null,
233
+ weightBinBytes: weightLayout?.byteLength ?? null
234
+ };
235
+ }
236
+
237
+ export {
238
+ NCNN_BINARY_PARAM_MAGIC,
239
+ NCNN_WEIGHT_FP16_STORAGE_TAG,
240
+ buildAllenkFdncnnWeightLayout,
241
+ halfToFloat,
242
+ parseAllenkFdncnnParam,
243
+ summarizeAllenkFdncnnModel
244
+ };