@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.
- package/README.md +17 -10
- package/README_zh.md +8 -1
- package/package.json +74 -3
- 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,337 @@
|
|
|
1
|
+
import { halfToFloat } from './allenkFdncnnNcnnModel.js';
|
|
2
|
+
|
|
3
|
+
const ONNX_IR_VERSION = 8;
|
|
4
|
+
const ONNX_OPSET_VERSION = 13;
|
|
5
|
+
const ONNX_TENSOR_FLOAT = 1;
|
|
6
|
+
const ONNX_ATTR_INTS = 7;
|
|
7
|
+
|
|
8
|
+
const WIRE_VARINT = 0;
|
|
9
|
+
const WIRE_LENGTH_DELIMITED = 2;
|
|
10
|
+
|
|
11
|
+
function normalizeBytes(bytes) {
|
|
12
|
+
if (bytes instanceof Uint8Array) return bytes;
|
|
13
|
+
return new Uint8Array(bytes);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function concatBytes(parts) {
|
|
17
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
18
|
+
const out = new Uint8Array(total);
|
|
19
|
+
let offset = 0;
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
out.set(part, offset);
|
|
22
|
+
offset += part.length;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function encodeVarint(value) {
|
|
28
|
+
let next = BigInt(value);
|
|
29
|
+
if (next < 0n) {
|
|
30
|
+
throw new Error(`Cannot encode negative varint: ${value}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bytes = [];
|
|
34
|
+
while (next >= 0x80n) {
|
|
35
|
+
bytes.push(Number((next & 0x7fn) | 0x80n));
|
|
36
|
+
next >>= 7n;
|
|
37
|
+
}
|
|
38
|
+
bytes.push(Number(next));
|
|
39
|
+
return Uint8Array.from(bytes);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fieldKey(fieldNumber, wireType) {
|
|
43
|
+
return encodeVarint((BigInt(fieldNumber) << 3n) | BigInt(wireType));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function varintField(fieldNumber, value) {
|
|
47
|
+
return concatBytes([fieldKey(fieldNumber, WIRE_VARINT), encodeVarint(value)]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function int64Field(fieldNumber, value) {
|
|
51
|
+
return varintField(fieldNumber, BigInt(value));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bytesField(fieldNumber, bytesLike) {
|
|
55
|
+
const bytes = normalizeBytes(bytesLike);
|
|
56
|
+
return concatBytes([
|
|
57
|
+
fieldKey(fieldNumber, WIRE_LENGTH_DELIMITED),
|
|
58
|
+
encodeVarint(bytes.length),
|
|
59
|
+
bytes
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stringField(fieldNumber, value) {
|
|
64
|
+
return bytesField(fieldNumber, new TextEncoder().encode(String(value)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function messageField(fieldNumber, message) {
|
|
68
|
+
return bytesField(fieldNumber, message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function message(parts) {
|
|
72
|
+
return concatBytes(parts.filter(Boolean));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function float32RawData(values) {
|
|
76
|
+
const out = new Uint8Array(values.length * 4);
|
|
77
|
+
const view = new DataView(out.buffer);
|
|
78
|
+
for (let i = 0; i < values.length; i++) {
|
|
79
|
+
view.setFloat32(i * 4, values[i], true);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readFloat32LE(bytes, offset) {
|
|
85
|
+
const buffer = normalizeBytes(bytes);
|
|
86
|
+
return new DataView(buffer.buffer, buffer.byteOffset + offset, 4).getFloat32(0, true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readUInt16LE(bytes, offset) {
|
|
90
|
+
const buffer = normalizeBytes(bytes);
|
|
91
|
+
return new DataView(buffer.buffer, buffer.byteOffset + offset, 2).getUint16(0, true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createTensorProto({ name, dims, rawData }) {
|
|
95
|
+
return message([
|
|
96
|
+
...dims.map((dim) => int64Field(1, dim)),
|
|
97
|
+
varintField(2, ONNX_TENSOR_FLOAT),
|
|
98
|
+
stringField(8, name),
|
|
99
|
+
bytesField(9, rawData)
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createShapeProto(dims) {
|
|
104
|
+
return message(dims.map((dim) => messageField(1, message([int64Field(1, dim)]))));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createValueInfoProto({ name, dims }) {
|
|
108
|
+
const tensorType = message([
|
|
109
|
+
varintField(1, ONNX_TENSOR_FLOAT),
|
|
110
|
+
messageField(2, createShapeProto(dims))
|
|
111
|
+
]);
|
|
112
|
+
const typeProto = message([messageField(1, tensorType)]);
|
|
113
|
+
|
|
114
|
+
return message([
|
|
115
|
+
stringField(1, name),
|
|
116
|
+
messageField(2, typeProto)
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createIntsAttribute(name, values) {
|
|
121
|
+
return message([
|
|
122
|
+
stringField(1, name),
|
|
123
|
+
...values.map((value) => int64Field(8, value)),
|
|
124
|
+
varintField(20, ONNX_ATTR_INTS)
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createNodeProto({ name, opType, inputs, outputs, attributes = [] }) {
|
|
129
|
+
return message([
|
|
130
|
+
...inputs.map((input) => stringField(1, input)),
|
|
131
|
+
...outputs.map((output) => stringField(2, output)),
|
|
132
|
+
stringField(3, name),
|
|
133
|
+
stringField(4, opType),
|
|
134
|
+
...attributes.map((attribute) => messageField(5, attribute))
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function decodeSegmentWeightsToFloat32(bin, segment) {
|
|
139
|
+
const values = new Array(segment.weightCount);
|
|
140
|
+
for (let i = 0; i < segment.weightCount; i++) {
|
|
141
|
+
values[i] = halfToFloat(readUInt16LE(bin, segment.weightOffset + i * 2));
|
|
142
|
+
}
|
|
143
|
+
return float32RawData(values);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function decodeSegmentBiasToFloat32(bin, segment) {
|
|
147
|
+
const values = new Array(segment.biasCount);
|
|
148
|
+
for (let i = 0; i < segment.biasCount; i++) {
|
|
149
|
+
values[i] = readFloat32LE(bin, segment.biasOffset + i * 4);
|
|
150
|
+
}
|
|
151
|
+
return float32RawData(values);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createInitializers({ bin, segments }) {
|
|
155
|
+
const initializers = [];
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < segments.length; i++) {
|
|
158
|
+
const segment = segments[i];
|
|
159
|
+
initializers.push({
|
|
160
|
+
name: `conv${i + 1}.weight`,
|
|
161
|
+
tensor: createTensorProto({
|
|
162
|
+
name: `conv${i + 1}.weight`,
|
|
163
|
+
dims: [
|
|
164
|
+
segment.outputChannels,
|
|
165
|
+
segment.inputChannels,
|
|
166
|
+
segment.kernelH,
|
|
167
|
+
segment.kernelW
|
|
168
|
+
],
|
|
169
|
+
rawData: decodeSegmentWeightsToFloat32(bin, segment)
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
initializers.push({
|
|
173
|
+
name: `conv${i + 1}.bias`,
|
|
174
|
+
tensor: createTensorProto({
|
|
175
|
+
name: `conv${i + 1}.bias`,
|
|
176
|
+
dims: [segment.biasCount],
|
|
177
|
+
rawData: decodeSegmentBiasToFloat32(bin, segment)
|
|
178
|
+
})
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return initializers;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createNodes({ segments, inputName, outputName }) {
|
|
186
|
+
const nodes = [];
|
|
187
|
+
let previous = inputName;
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < segments.length; i++) {
|
|
190
|
+
const segment = segments[i];
|
|
191
|
+
const index = i + 1;
|
|
192
|
+
const isLast = i === segments.length - 1;
|
|
193
|
+
const convOutput = isLast ? outputName : `conv${index}.out`;
|
|
194
|
+
const reluOutput = `relu${index}.out`;
|
|
195
|
+
|
|
196
|
+
nodes.push(createNodeProto({
|
|
197
|
+
name: `conv${index}`,
|
|
198
|
+
opType: 'Conv',
|
|
199
|
+
inputs: [
|
|
200
|
+
previous,
|
|
201
|
+
`conv${index}.weight`,
|
|
202
|
+
`conv${index}.bias`
|
|
203
|
+
],
|
|
204
|
+
outputs: [convOutput],
|
|
205
|
+
attributes: [
|
|
206
|
+
createIntsAttribute('kernel_shape', [segment.kernelH, segment.kernelW]),
|
|
207
|
+
createIntsAttribute('pads', [segment.padH, segment.padW, segment.padH, segment.padW]),
|
|
208
|
+
createIntsAttribute('strides', [segment.strideH, segment.strideW])
|
|
209
|
+
]
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
if (!isLast && segment.activationType === 1) {
|
|
213
|
+
nodes.push(createNodeProto({
|
|
214
|
+
name: `relu${index}`,
|
|
215
|
+
opType: 'Relu',
|
|
216
|
+
inputs: [convOutput],
|
|
217
|
+
outputs: [reluOutput]
|
|
218
|
+
}));
|
|
219
|
+
previous = reluOutput;
|
|
220
|
+
} else {
|
|
221
|
+
previous = convOutput;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return nodes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createGraphProto({
|
|
229
|
+
bin,
|
|
230
|
+
name,
|
|
231
|
+
segments,
|
|
232
|
+
inputName,
|
|
233
|
+
outputName,
|
|
234
|
+
roiSize
|
|
235
|
+
}) {
|
|
236
|
+
const initializers = createInitializers({ bin, segments });
|
|
237
|
+
const nodes = createNodes({ segments, inputName, outputName });
|
|
238
|
+
const inputShape = [1, segments[0].inputChannels, roiSize, roiSize];
|
|
239
|
+
const outputShape = [1, segments[segments.length - 1].outputChannels, roiSize, roiSize];
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
nodeCount: nodes.length,
|
|
243
|
+
initializerCount: initializers.length,
|
|
244
|
+
bytes: message([
|
|
245
|
+
...nodes.map((node) => messageField(1, node)),
|
|
246
|
+
stringField(2, name),
|
|
247
|
+
...initializers.map((initializer) => messageField(5, initializer.tensor)),
|
|
248
|
+
messageField(11, createValueInfoProto({ name: inputName, dims: inputShape })),
|
|
249
|
+
messageField(12, createValueInfoProto({ name: outputName, dims: outputShape }))
|
|
250
|
+
])
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createOpsetImport(version = ONNX_OPSET_VERSION) {
|
|
255
|
+
return message([int64Field(2, version)]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createModelProto({
|
|
259
|
+
graph,
|
|
260
|
+
irVersion = ONNX_IR_VERSION,
|
|
261
|
+
opsetVersion = ONNX_OPSET_VERSION,
|
|
262
|
+
producerName = 'gemini-watermark-remover',
|
|
263
|
+
modelVersion = 1
|
|
264
|
+
}) {
|
|
265
|
+
return message([
|
|
266
|
+
int64Field(1, irVersion),
|
|
267
|
+
stringField(2, producerName),
|
|
268
|
+
int64Field(5, modelVersion),
|
|
269
|
+
messageField(7, graph),
|
|
270
|
+
messageField(8, createOpsetImport(opsetVersion))
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function exportAllenkFdncnnOnnx({
|
|
275
|
+
bin,
|
|
276
|
+
weightLayout,
|
|
277
|
+
roiSize = 72,
|
|
278
|
+
inputName = 'fdncnn_input',
|
|
279
|
+
outputName = 'fdncnn_output',
|
|
280
|
+
graphName = 'allenk_fdncnn_color',
|
|
281
|
+
opsetVersion = ONNX_OPSET_VERSION,
|
|
282
|
+
irVersion = ONNX_IR_VERSION
|
|
283
|
+
} = {}) {
|
|
284
|
+
const segments = weightLayout?.segments || [];
|
|
285
|
+
if (!segments.length) {
|
|
286
|
+
throw new Error('allenk FDnCNN ONNX export requires weightLayout.segments');
|
|
287
|
+
}
|
|
288
|
+
if (!Number.isInteger(roiSize) || roiSize <= 0) {
|
|
289
|
+
throw new Error(`Invalid ONNX ROI size: ${roiSize}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const graph = createGraphProto({
|
|
293
|
+
bin: normalizeBytes(bin),
|
|
294
|
+
name: graphName,
|
|
295
|
+
segments,
|
|
296
|
+
inputName,
|
|
297
|
+
outputName,
|
|
298
|
+
roiSize
|
|
299
|
+
});
|
|
300
|
+
const model = createModelProto({
|
|
301
|
+
graph: graph.bytes,
|
|
302
|
+
irVersion,
|
|
303
|
+
opsetVersion
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
bytes: model,
|
|
308
|
+
metadata: {
|
|
309
|
+
format: 'onnx',
|
|
310
|
+
tensorDataType: 'float32-raw-data',
|
|
311
|
+
irVersion,
|
|
312
|
+
opsetVersion,
|
|
313
|
+
graphName,
|
|
314
|
+
inputName,
|
|
315
|
+
outputName,
|
|
316
|
+
inputShape: [1, segments[0].inputChannels, roiSize, roiSize],
|
|
317
|
+
outputShape: [1, segments[segments.length - 1].outputChannels, roiSize, roiSize],
|
|
318
|
+
nodeCount: graph.nodeCount,
|
|
319
|
+
convolutionNodeCount: segments.length,
|
|
320
|
+
reluNodeCount: segments.filter((segment, index) => index < segments.length - 1 && segment.activationType === 1).length,
|
|
321
|
+
initializerCount: graph.initializerCount,
|
|
322
|
+
roiSize
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export {
|
|
328
|
+
ONNX_IR_VERSION,
|
|
329
|
+
ONNX_OPSET_VERSION,
|
|
330
|
+
concatBytes,
|
|
331
|
+
createIntsAttribute,
|
|
332
|
+
createNodeProto,
|
|
333
|
+
createTensorProto,
|
|
334
|
+
encodeVarint,
|
|
335
|
+
exportAllenkFdncnnOnnx,
|
|
336
|
+
varintField
|
|
337
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as wasmOrt from 'onnxruntime-web/wasm';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAllenkFdncnnInput,
|
|
5
|
+
convertAllenkFdncnnOutputToRgba
|
|
6
|
+
} from './allenkFdncnnDenoise.js';
|
|
7
|
+
|
|
8
|
+
function getNow() {
|
|
9
|
+
return globalThis.performance?.now ? globalThis.performance.now() : Date.now();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeShape(shape, fallback = null) {
|
|
13
|
+
if (!Array.isArray(shape)) return fallback;
|
|
14
|
+
const normalized = shape.map((value) => Number(value));
|
|
15
|
+
return normalized.every((value) => Number.isInteger(value) && value > 0)
|
|
16
|
+
? normalized
|
|
17
|
+
: fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateImageShape(imageData, inputShape) {
|
|
21
|
+
const expectedHeight = inputShape?.[2];
|
|
22
|
+
const expectedWidth = inputShape?.[3];
|
|
23
|
+
if (!imageData?.data || imageData.width <= 0 || imageData.height <= 0) {
|
|
24
|
+
throw new Error('allenk FDnCNN ONNX runtime requires ImageData-like input');
|
|
25
|
+
}
|
|
26
|
+
if (imageData.width !== expectedWidth || imageData.height !== expectedHeight) {
|
|
27
|
+
const error = new Error(
|
|
28
|
+
`allenk FDnCNN ONNX runtime expected ${expectedWidth}x${expectedHeight}, got ${imageData.width}x${imageData.height}`
|
|
29
|
+
);
|
|
30
|
+
error.code = 'ALLENK_FDNCNN_ONNX_SHAPE_MISMATCH';
|
|
31
|
+
error.expectedWidth = expectedWidth;
|
|
32
|
+
error.expectedHeight = expectedHeight;
|
|
33
|
+
error.actualWidth = imageData.width;
|
|
34
|
+
error.actualHeight = imageData.height;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function createAllenkFdncnnOnnxRuntime({
|
|
40
|
+
ort = null,
|
|
41
|
+
modelBytes,
|
|
42
|
+
session = null,
|
|
43
|
+
executionProvider = 'wasm',
|
|
44
|
+
inputName = 'fdncnn_input',
|
|
45
|
+
outputName = 'fdncnn_output',
|
|
46
|
+
inputShape = [1, 4, 72, 72],
|
|
47
|
+
outputShape = [1, 3, 72, 72],
|
|
48
|
+
graphOptimizationLevel = 'all',
|
|
49
|
+
wasmPaths = null,
|
|
50
|
+
numThreads = 'auto'
|
|
51
|
+
} = {}) {
|
|
52
|
+
if (executionProvider === 'webgpu' && !ort) {
|
|
53
|
+
throw new Error('allenk FDnCNN WebGPU runtime requires an injected onnxruntime-web/webgpu module');
|
|
54
|
+
}
|
|
55
|
+
const resolvedOrt = ort || wasmOrt;
|
|
56
|
+
const resolvedInputShape = normalizeShape(inputShape, [1, 4, 72, 72]);
|
|
57
|
+
const resolvedOutputShape = normalizeShape(outputShape, [1, 3, resolvedInputShape[2], resolvedInputShape[3]]);
|
|
58
|
+
if (resolvedOrt.env?.wasm) {
|
|
59
|
+
const canUseThreads = Boolean(globalThis.crossOriginIsolated && typeof SharedArrayBuffer !== 'undefined');
|
|
60
|
+
const hardwareThreads = Number(globalThis.navigator?.hardwareConcurrency) || 1;
|
|
61
|
+
const resolvedNumThreads = numThreads === 'auto'
|
|
62
|
+
? (canUseThreads ? Math.max(1, Math.min(4, Math.ceil(hardwareThreads / 2))) : 1)
|
|
63
|
+
: Math.max(1, Math.round(Number(numThreads) || 1));
|
|
64
|
+
resolvedOrt.env.wasm.numThreads = executionProvider === 'wasm' ? resolvedNumThreads : 1;
|
|
65
|
+
resolvedOrt.env.wasm.proxy = false;
|
|
66
|
+
if (wasmPaths) {
|
|
67
|
+
resolvedOrt.env.wasm.wasmPaths = wasmPaths;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (executionProvider === 'webgpu' && resolvedOrt.env?.webgpu) {
|
|
71
|
+
resolvedOrt.env.webgpu.powerPreference = 'high-performance';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const createStarted = getNow();
|
|
75
|
+
const resolvedSession = session || await resolvedOrt.InferenceSession.create(modelBytes, {
|
|
76
|
+
executionProviders: [executionProvider],
|
|
77
|
+
graphOptimizationLevel
|
|
78
|
+
});
|
|
79
|
+
const createMs = getNow() - createStarted;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: `allenk-fdncnn-onnx-${executionProvider}`,
|
|
83
|
+
status: 'prototype',
|
|
84
|
+
executionProvider,
|
|
85
|
+
inputName,
|
|
86
|
+
outputName,
|
|
87
|
+
inputShape: resolvedInputShape,
|
|
88
|
+
outputShape: resolvedOutputShape,
|
|
89
|
+
createMs,
|
|
90
|
+
session: resolvedSession,
|
|
91
|
+
numThreads: executionProvider === 'wasm'
|
|
92
|
+
? resolvedOrt.env?.wasm?.numThreads ?? null
|
|
93
|
+
: null,
|
|
94
|
+
estimateMacs(width, height) {
|
|
95
|
+
// Matches the decoded allenk FDnCNN graph: 4->64, 18x 64->64, 64->3, all 3x3.
|
|
96
|
+
return width * height * ((4 * 64 * 9) + (18 * 64 * 64 * 9) + (64 * 3 * 9));
|
|
97
|
+
},
|
|
98
|
+
async execute(input) {
|
|
99
|
+
const tensor = new resolvedOrt.Tensor('float32', input, resolvedInputShape);
|
|
100
|
+
const started = getNow();
|
|
101
|
+
const outputs = await resolvedSession.run({ [inputName]: tensor });
|
|
102
|
+
const runMs = getNow() - started;
|
|
103
|
+
const outputTensor = outputs[outputName];
|
|
104
|
+
if (!outputTensor?.data) {
|
|
105
|
+
throw new Error(`allenk FDnCNN ONNX runtime did not return ${outputName}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
output: outputTensor.data,
|
|
109
|
+
outputShape: [...outputTensor.dims],
|
|
110
|
+
runtime: this.id,
|
|
111
|
+
runMs,
|
|
112
|
+
macs: this.estimateMacs(resolvedInputShape[3], resolvedInputShape[2])
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
async denoiseImageData({ imageData, sigma } = {}) {
|
|
116
|
+
validateImageShape(imageData, resolvedInputShape);
|
|
117
|
+
const input = buildAllenkFdncnnInput({ imageData, sigma });
|
|
118
|
+
const result = await this.execute(input);
|
|
119
|
+
return {
|
|
120
|
+
...result,
|
|
121
|
+
imageData: {
|
|
122
|
+
width: imageData.width,
|
|
123
|
+
height: imageData.height,
|
|
124
|
+
data: convertAllenkFdncnnOutputToRgba({
|
|
125
|
+
output: result.output,
|
|
126
|
+
width: imageData.width,
|
|
127
|
+
height: imageData.height
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
createAllenkFdncnnOnnxRuntime,
|
|
137
|
+
validateImageShape
|
|
138
|
+
};
|
|
@@ -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
|
+
};
|