@srsergio/taptapp-ar 1.0.2 → 1.0.3

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.
Files changed (83) hide show
  1. package/README.md +47 -45
  2. package/dist/compiler/aframe.js +0 -3
  3. package/dist/compiler/compiler-base.d.ts +3 -7
  4. package/dist/compiler/compiler-base.js +28 -14
  5. package/dist/compiler/compiler.js +1 -1
  6. package/dist/compiler/compiler.worker.js +1 -1
  7. package/dist/compiler/controller.js +4 -5
  8. package/dist/compiler/controller.worker.js +0 -2
  9. package/dist/compiler/detector/crop-detector.js +0 -2
  10. package/dist/compiler/detector/detector-lite.d.ts +73 -0
  11. package/dist/compiler/detector/detector-lite.js +430 -0
  12. package/dist/compiler/detector/detector.js +236 -243
  13. package/dist/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
  14. package/dist/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
  15. package/dist/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -18
  16. package/dist/compiler/detector/kernels/cpu/fakeShader.js +1 -1
  17. package/dist/compiler/detector/kernels/cpu/prune.d.ts +7 -1
  18. package/dist/compiler/detector/kernels/cpu/prune.js +1 -42
  19. package/dist/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
  20. package/dist/compiler/estimation/refine-estimate.js +0 -1
  21. package/dist/compiler/estimation/utils.d.ts +1 -1
  22. package/dist/compiler/estimation/utils.js +1 -14
  23. package/dist/compiler/image-list.js +4 -4
  24. package/dist/compiler/input-loader.js +2 -2
  25. package/dist/compiler/matching/hamming-distance.js +13 -13
  26. package/dist/compiler/matching/hierarchical-clustering.js +1 -1
  27. package/dist/compiler/matching/matching.d.ts +20 -4
  28. package/dist/compiler/matching/matching.js +67 -41
  29. package/dist/compiler/matching/ransacHomography.js +1 -2
  30. package/dist/compiler/node-worker.d.ts +1 -0
  31. package/dist/compiler/node-worker.js +84 -0
  32. package/dist/compiler/offline-compiler.d.ts +171 -6
  33. package/dist/compiler/offline-compiler.js +303 -421
  34. package/dist/compiler/tensorflow-setup.js +27 -1
  35. package/dist/compiler/three.js +3 -5
  36. package/dist/compiler/tracker/extract.d.ts +1 -0
  37. package/dist/compiler/tracker/extract.js +200 -244
  38. package/dist/compiler/tracker/tracker.d.ts +1 -1
  39. package/dist/compiler/tracker/tracker.js +13 -18
  40. package/dist/compiler/utils/cumsum.d.ts +4 -2
  41. package/dist/compiler/utils/cumsum.js +17 -19
  42. package/dist/compiler/utils/gpu-compute.d.ts +57 -0
  43. package/dist/compiler/utils/gpu-compute.js +262 -0
  44. package/dist/compiler/utils/images.d.ts +4 -4
  45. package/dist/compiler/utils/images.js +67 -53
  46. package/dist/compiler/utils/worker-pool.d.ts +14 -0
  47. package/dist/compiler/utils/worker-pool.js +84 -0
  48. package/package.json +11 -13
  49. package/src/compiler/aframe.js +2 -4
  50. package/src/compiler/compiler-base.js +29 -14
  51. package/src/compiler/compiler.js +1 -1
  52. package/src/compiler/compiler.worker.js +1 -1
  53. package/src/compiler/controller.js +4 -5
  54. package/src/compiler/controller.worker.js +0 -2
  55. package/src/compiler/detector/crop-detector.js +0 -2
  56. package/src/compiler/detector/detector-lite.js +494 -0
  57. package/src/compiler/detector/detector.js +1052 -1063
  58. package/src/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
  59. package/src/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
  60. package/src/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -17
  61. package/src/compiler/detector/kernels/cpu/fakeShader.js +1 -1
  62. package/src/compiler/detector/kernels/cpu/prune.js +1 -37
  63. package/src/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
  64. package/src/compiler/estimation/refine-estimate.js +0 -1
  65. package/src/compiler/estimation/utils.js +9 -24
  66. package/src/compiler/image-list.js +4 -4
  67. package/src/compiler/input-loader.js +2 -2
  68. package/src/compiler/matching/hamming-distance.js +11 -15
  69. package/src/compiler/matching/hierarchical-clustering.js +1 -1
  70. package/src/compiler/matching/matching.js +72 -42
  71. package/src/compiler/matching/ransacHomography.js +0 -2
  72. package/src/compiler/node-worker.js +93 -0
  73. package/src/compiler/offline-compiler.js +339 -504
  74. package/src/compiler/tensorflow-setup.js +29 -1
  75. package/src/compiler/three.js +3 -5
  76. package/src/compiler/tracker/extract.js +211 -267
  77. package/src/compiler/tracker/tracker.js +13 -22
  78. package/src/compiler/utils/cumsum.js +17 -19
  79. package/src/compiler/utils/gpu-compute.js +303 -0
  80. package/src/compiler/utils/images.js +84 -53
  81. package/src/compiler/utils/worker-pool.js +89 -0
  82. package/src/compiler/estimation/esimate-experiment.js +0 -316
  83. package/src/compiler/estimation/refine-estimate-experiment.js +0 -512
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Detector Lite - Pure JavaScript Feature Detector
3
+ *
4
+ * Un detector de características simplificado que no depende de TensorFlow.
5
+ * Optimizado para velocidad en compilación offline.
6
+ *
7
+ * Implementa:
8
+ * - Construcción de pirámide gaussiana (con aceleración GPU opcional)
9
+ * - Diferencia de Gaussianas (DoG) para detección de extremos
10
+ * - Descriptores FREAK simplificados
11
+ */
12
+ import { FREAKPOINTS } from "./freak.js";
13
+ import { gpuCompute } from "../utils/gpu-compute.js";
14
+ const PYRAMID_MIN_SIZE = 8;
15
+ const PYRAMID_MAX_OCTAVE = 5;
16
+ const NUM_BUCKETS_PER_DIMENSION = 8;
17
+ const MAX_FEATURES_PER_BUCKET = 3; // Optimizado: Reducido de 5 a 3 para menor peso
18
+ const ORIENTATION_NUM_BINS = 36;
19
+ const FREAK_EXPANSION_FACTOR = 7.0;
20
+ // Global GPU mode flag
21
+ let globalUseGPU = true;
22
+ /**
23
+ * Set global GPU mode for all DetectorLite instances
24
+ * @param {boolean} enabled - Whether to use GPU acceleration
25
+ */
26
+ export const setDetectorGPUMode = (enabled) => {
27
+ globalUseGPU = enabled;
28
+ };
29
+ /**
30
+ * Detector de características sin TensorFlow
31
+ */
32
+ export class DetectorLite {
33
+ constructor(width, height, options = {}) {
34
+ this.width = width;
35
+ this.height = height;
36
+ this.useGPU = options.useGPU !== undefined ? options.useGPU : globalUseGPU;
37
+ let numOctaves = 0;
38
+ let w = width, h = height;
39
+ while (w >= PYRAMID_MIN_SIZE && h >= PYRAMID_MIN_SIZE) {
40
+ w = Math.floor(w / 2);
41
+ h = Math.floor(h / 2);
42
+ numOctaves++;
43
+ if (numOctaves === PYRAMID_MAX_OCTAVE)
44
+ break;
45
+ }
46
+ this.numOctaves = numOctaves;
47
+ }
48
+ /**
49
+ * Detecta características en una imagen en escala de grises
50
+ * @param {Float32Array|Uint8Array} imageData - Datos de imagen (width * height)
51
+ * @returns {{featurePoints: Array}} Puntos de características detectados
52
+ */
53
+ detect(imageData) {
54
+ // Normalizar a Float32Array si es necesario
55
+ let data;
56
+ if (imageData instanceof Float32Array) {
57
+ data = imageData;
58
+ }
59
+ else {
60
+ data = new Float32Array(imageData.length);
61
+ for (let i = 0; i < imageData.length; i++) {
62
+ data[i] = imageData[i];
63
+ }
64
+ }
65
+ // 1. Construir pirámide gaussiana
66
+ const pyramidImages = this._buildGaussianPyramid(data, this.width, this.height);
67
+ // 2. Construir pirámide DoG (Difference of Gaussians)
68
+ const dogPyramid = this._buildDogPyramid(pyramidImages);
69
+ // 3. Encontrar extremos locales
70
+ const extremas = this._findExtremas(dogPyramid, pyramidImages);
71
+ // 4. Aplicar pruning por buckets
72
+ const prunedExtremas = this._applyPrune(extremas);
73
+ // 5. Calcular orientaciones
74
+ this._computeOrientations(prunedExtremas, pyramidImages);
75
+ // 6. Calcular descriptores FREAK
76
+ this._computeFreakDescriptors(prunedExtremas, pyramidImages);
77
+ // Convertir a formato de salida
78
+ const featurePoints = prunedExtremas.map(ext => ({
79
+ maxima: ext.score > 0,
80
+ x: ext.x * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
81
+ y: ext.y * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
82
+ scale: Math.pow(2, ext.octave),
83
+ angle: ext.angle || 0,
84
+ descriptors: ext.descriptors || []
85
+ }));
86
+ return { featurePoints };
87
+ }
88
+ /**
89
+ * Construye una pirámide gaussiana
90
+ */
91
+ _buildGaussianPyramid(data, width, height) {
92
+ // Use GPU-accelerated pyramid if available
93
+ if (this.useGPU) {
94
+ try {
95
+ const gpuPyramid = gpuCompute.buildPyramid(data, width, height, this.numOctaves);
96
+ // Convert GPU pyramid format to expected format
97
+ const pyramid = [];
98
+ for (let i = 0; i < gpuPyramid.length && i < this.numOctaves; i++) {
99
+ const level = gpuPyramid[i];
100
+ // Apply second blur for DoG computation
101
+ const img2 = this._applyGaussianFilter(level.data, level.width, level.height);
102
+ pyramid.push([
103
+ { data: level.data, width: level.width, height: level.height },
104
+ { data: img2.data, width: level.width, height: level.height }
105
+ ]);
106
+ }
107
+ return pyramid;
108
+ }
109
+ catch (e) {
110
+ // Fall back to CPU if GPU fails
111
+ console.warn("GPU pyramid failed, falling back to CPU:", e.message);
112
+ }
113
+ }
114
+ // Original CPU implementation
115
+ const pyramid = [];
116
+ let currentData = data;
117
+ let currentWidth = width;
118
+ let currentHeight = height;
119
+ for (let i = 0; i < this.numOctaves; i++) {
120
+ const img1 = this._applyGaussianFilter(currentData, currentWidth, currentHeight);
121
+ const img2 = this._applyGaussianFilter(img1.data, currentWidth, currentHeight);
122
+ pyramid.push([
123
+ { data: img1.data, width: currentWidth, height: currentHeight },
124
+ { data: img2.data, width: currentWidth, height: currentHeight }
125
+ ]);
126
+ // Downsample para siguiente octava
127
+ if (i < this.numOctaves - 1) {
128
+ const downsampled = this._downsample(img2.data, currentWidth, currentHeight);
129
+ currentData = downsampled.data;
130
+ currentWidth = downsampled.width;
131
+ currentHeight = downsampled.height;
132
+ }
133
+ }
134
+ return pyramid;
135
+ }
136
+ /**
137
+ * Aplica un filtro gaussiano binomial [1,4,6,4,1] - Optimizado
138
+ */
139
+ _applyGaussianFilter(data, width, height) {
140
+ const output = new Float32Array(width * height);
141
+ const temp = new Float32Array(width * height);
142
+ const k0 = 1 / 16, k1 = 4 / 16, k2 = 6 / 16;
143
+ const w1 = width - 1;
144
+ const h1 = height - 1;
145
+ // Horizontal pass - unrolled kernel
146
+ for (let y = 0; y < height; y++) {
147
+ const rowOffset = y * width;
148
+ for (let x = 0; x < width; x++) {
149
+ const x0 = x < 2 ? 0 : x - 2;
150
+ const x1 = x < 1 ? 0 : x - 1;
151
+ const x3 = x > w1 - 1 ? w1 : x + 1;
152
+ const x4 = x > w1 - 2 ? w1 : x + 2;
153
+ temp[rowOffset + x] =
154
+ data[rowOffset + x0] * k0 +
155
+ data[rowOffset + x1] * k1 +
156
+ data[rowOffset + x] * k2 +
157
+ data[rowOffset + x3] * k1 +
158
+ data[rowOffset + x4] * k0;
159
+ }
160
+ }
161
+ // Vertical pass - unrolled kernel
162
+ for (let y = 0; y < height; y++) {
163
+ const y0 = (y < 2 ? 0 : y - 2) * width;
164
+ const y1 = (y < 1 ? 0 : y - 1) * width;
165
+ const y2 = y * width;
166
+ const y3 = (y > h1 - 1 ? h1 : y + 1) * width;
167
+ const y4 = (y > h1 - 2 ? h1 : y + 2) * width;
168
+ for (let x = 0; x < width; x++) {
169
+ output[y2 + x] =
170
+ temp[y0 + x] * k0 +
171
+ temp[y1 + x] * k1 +
172
+ temp[y2 + x] * k2 +
173
+ temp[y3 + x] * k1 +
174
+ temp[y4 + x] * k0;
175
+ }
176
+ }
177
+ return { data: output, width, height };
178
+ }
179
+ /**
180
+ * Downsample imagen por factor de 2
181
+ */
182
+ _downsample(data, width, height) {
183
+ const newWidth = Math.floor(width / 2);
184
+ const newHeight = Math.floor(height / 2);
185
+ const output = new Float32Array(newWidth * newHeight);
186
+ for (let y = 0; y < newHeight; y++) {
187
+ for (let x = 0; x < newWidth; x++) {
188
+ // Interpolación bilinear
189
+ const srcX = x * 2 + 0.5;
190
+ const srcY = y * 2 + 0.5;
191
+ const x0 = Math.floor(srcX);
192
+ const y0 = Math.floor(srcY);
193
+ const x1 = Math.min(x0 + 1, width - 1);
194
+ const y1 = Math.min(y0 + 1, height - 1);
195
+ const fx = srcX - x0;
196
+ const fy = srcY - y0;
197
+ const v00 = data[y0 * width + x0];
198
+ const v10 = data[y0 * width + x1];
199
+ const v01 = data[y1 * width + x0];
200
+ const v11 = data[y1 * width + x1];
201
+ output[y * newWidth + x] =
202
+ v00 * (1 - fx) * (1 - fy) +
203
+ v10 * fx * (1 - fy) +
204
+ v01 * (1 - fx) * fy +
205
+ v11 * fx * fy;
206
+ }
207
+ }
208
+ return { data: output, width: newWidth, height: newHeight };
209
+ }
210
+ /**
211
+ * Construye pirámide de diferencia de gaussianas
212
+ */
213
+ _buildDogPyramid(pyramidImages) {
214
+ const dogPyramid = [];
215
+ for (let i = 0; i < pyramidImages.length; i++) {
216
+ const img1 = pyramidImages[i][0];
217
+ const img2 = pyramidImages[i][1];
218
+ const width = img1.width;
219
+ const height = img1.height;
220
+ const dog = new Float32Array(width * height);
221
+ for (let j = 0; j < dog.length; j++) {
222
+ dog[j] = img2.data[j] - img1.data[j];
223
+ }
224
+ dogPyramid.push({ data: dog, width, height });
225
+ }
226
+ return dogPyramid;
227
+ }
228
+ /**
229
+ * Encuentra extremos locales en la pirámide DoG
230
+ */
231
+ _findExtremas(dogPyramid, pyramidImages) {
232
+ const extremas = [];
233
+ for (let octave = 1; octave < dogPyramid.length - 1; octave++) {
234
+ const curr = dogPyramid[octave];
235
+ const prev = dogPyramid[octave - 1];
236
+ const next = dogPyramid[octave + 1];
237
+ const width = curr.width;
238
+ const height = curr.height;
239
+ const prevWidth = prev.width;
240
+ const nextWidth = next.width;
241
+ for (let y = 1; y < height - 1; y++) {
242
+ for (let x = 1; x < width - 1; x++) {
243
+ const val = curr.data[y * width + x];
244
+ if (Math.abs(val) < 0.015)
245
+ continue; // Threshold
246
+ let isMaxima = true;
247
+ let isMinima = true;
248
+ // Check 3x3 neighborhood in current scale
249
+ for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
250
+ for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
251
+ if (dx === 0 && dy === 0)
252
+ continue;
253
+ const neighbor = curr.data[(y + dy) * width + (x + dx)];
254
+ if (neighbor >= val)
255
+ isMaxima = false;
256
+ if (neighbor <= val)
257
+ isMinima = false;
258
+ }
259
+ }
260
+ // Check previous scale (scaled coordinates)
261
+ if (isMaxima || isMinima) {
262
+ const px = Math.floor(x * 2);
263
+ const py = Math.floor(y * 2);
264
+ for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
265
+ for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
266
+ const xx = Math.max(0, Math.min(prevWidth - 1, px + dx));
267
+ const yy = Math.max(0, Math.min(prev.height - 1, py + dy));
268
+ const neighbor = prev.data[yy * prevWidth + xx];
269
+ if (neighbor >= val)
270
+ isMaxima = false;
271
+ if (neighbor <= val)
272
+ isMinima = false;
273
+ }
274
+ }
275
+ }
276
+ // Check next scale (scaled coordinates)
277
+ if (isMaxima || isMinima) {
278
+ const nx = Math.floor(x / 2);
279
+ const ny = Math.floor(y / 2);
280
+ for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
281
+ for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
282
+ const xx = Math.max(0, Math.min(nextWidth - 1, nx + dx));
283
+ const yy = Math.max(0, Math.min(next.height - 1, ny + dy));
284
+ const neighbor = next.data[yy * nextWidth + xx];
285
+ if (neighbor >= val)
286
+ isMaxima = false;
287
+ if (neighbor <= val)
288
+ isMinima = false;
289
+ }
290
+ }
291
+ }
292
+ if (isMaxima || isMinima) {
293
+ extremas.push({
294
+ score: isMaxima ? Math.abs(val) : -Math.abs(val),
295
+ octave,
296
+ x,
297
+ y,
298
+ absScore: Math.abs(val)
299
+ });
300
+ }
301
+ }
302
+ }
303
+ }
304
+ return extremas;
305
+ }
306
+ /**
307
+ * Aplica pruning para mantener solo los mejores features por bucket
308
+ */
309
+ _applyPrune(extremas) {
310
+ const nBuckets = NUM_BUCKETS_PER_DIMENSION;
311
+ const nFeatures = MAX_FEATURES_PER_BUCKET;
312
+ // Agrupar por buckets
313
+ const buckets = [];
314
+ for (let i = 0; i < nBuckets * nBuckets; i++) {
315
+ buckets.push([]);
316
+ }
317
+ for (const ext of extremas) {
318
+ const bucketX = Math.min(nBuckets - 1, Math.floor(ext.x / (this.width / Math.pow(2, ext.octave)) * nBuckets));
319
+ const bucketY = Math.min(nBuckets - 1, Math.floor(ext.y / (this.height / Math.pow(2, ext.octave)) * nBuckets));
320
+ const bucketIdx = bucketY * nBuckets + bucketX;
321
+ if (bucketIdx >= 0 && bucketIdx < buckets.length) {
322
+ buckets[bucketIdx].push(ext);
323
+ }
324
+ }
325
+ // Seleccionar top features por bucket
326
+ const result = [];
327
+ for (const bucket of buckets) {
328
+ bucket.sort((a, b) => b.absScore - a.absScore);
329
+ for (let i = 0; i < Math.min(nFeatures, bucket.length); i++) {
330
+ result.push(bucket[i]);
331
+ }
332
+ }
333
+ return result;
334
+ }
335
+ /**
336
+ * Calcula la orientación de cada feature
337
+ */
338
+ _computeOrientations(extremas, pyramidImages) {
339
+ for (const ext of extremas) {
340
+ if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
341
+ ext.angle = 0;
342
+ continue;
343
+ }
344
+ const img = pyramidImages[ext.octave][1];
345
+ const width = img.width;
346
+ const height = img.height;
347
+ const data = img.data;
348
+ const x = Math.floor(ext.x);
349
+ const y = Math.floor(ext.y);
350
+ // Compute gradient histogram
351
+ const histogram = new Float32Array(ORIENTATION_NUM_BINS);
352
+ const radius = 4;
353
+ for (let dy = -radius; dy <= radius; dy++) {
354
+ for (let dx = -radius; dx <= radius; dx++) {
355
+ const yy = y + dy;
356
+ const xx = x + dx;
357
+ if (yy <= 0 || yy >= height - 1 || xx <= 0 || xx >= width - 1)
358
+ continue;
359
+ const gradY = data[(yy + 1) * width + xx] - data[(yy - 1) * width + xx];
360
+ const gradX = data[yy * width + xx + 1] - data[yy * width + xx - 1];
361
+ const mag = Math.sqrt(gradX * gradX + gradY * gradY);
362
+ const angle = Math.atan2(gradY, gradX) + Math.PI; // 0 to 2*PI
363
+ const bin = Math.floor(angle / (2 * Math.PI) * ORIENTATION_NUM_BINS) % ORIENTATION_NUM_BINS;
364
+ const weight = Math.exp(-(dx * dx + dy * dy) / (2 * radius * radius));
365
+ histogram[bin] += mag * weight;
366
+ }
367
+ }
368
+ // Find peak
369
+ let maxBin = 0;
370
+ for (let i = 1; i < ORIENTATION_NUM_BINS; i++) {
371
+ if (histogram[i] > histogram[maxBin]) {
372
+ maxBin = i;
373
+ }
374
+ }
375
+ ext.angle = (maxBin + 0.5) * 2 * Math.PI / ORIENTATION_NUM_BINS - Math.PI;
376
+ }
377
+ }
378
+ /**
379
+ * Calcula descriptores FREAK
380
+ */
381
+ _computeFreakDescriptors(extremas, pyramidImages) {
382
+ for (const ext of extremas) {
383
+ if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
384
+ ext.descriptors = [];
385
+ continue;
386
+ }
387
+ const img = pyramidImages[ext.octave][1];
388
+ const width = img.width;
389
+ const height = img.height;
390
+ const data = img.data;
391
+ const cos = Math.cos(ext.angle || 0) * FREAK_EXPANSION_FACTOR;
392
+ const sin = Math.sin(ext.angle || 0) * FREAK_EXPANSION_FACTOR;
393
+ // Sample FREAK points
394
+ const samples = new Float32Array(FREAKPOINTS.length);
395
+ for (let i = 0; i < FREAKPOINTS.length; i++) {
396
+ const [, fx, fy] = FREAKPOINTS[i];
397
+ const xp = ext.x + fx * cos - fy * sin;
398
+ const yp = ext.y + fx * sin + fy * cos;
399
+ const x0 = Math.max(0, Math.min(width - 2, Math.floor(xp)));
400
+ const y0 = Math.max(0, Math.min(height - 2, Math.floor(yp)));
401
+ const x1 = x0 + 1;
402
+ const y1 = y0 + 1;
403
+ const fracX = xp - x0;
404
+ const fracY = yp - y0;
405
+ samples[i] =
406
+ data[y0 * width + x0] * (1 - fracX) * (1 - fracY) +
407
+ data[y0 * width + x1] * fracX * (1 - fracY) +
408
+ data[y1 * width + x0] * (1 - fracX) * fracY +
409
+ data[y1 * width + x1] * fracX * fracY;
410
+ }
411
+ // Pack pairs into Uint8Array (84 bytes per descriptor)
412
+ const descriptor = new Uint8Array(84);
413
+ let bitCount = 0;
414
+ let byteIdx = 0;
415
+ for (let i = 0; i < FREAKPOINTS.length; i++) {
416
+ for (let j = i + 1; j < FREAKPOINTS.length; j++) {
417
+ if (samples[i] < samples[j]) {
418
+ descriptor[byteIdx] |= (1 << (7 - bitCount));
419
+ }
420
+ bitCount++;
421
+ if (bitCount === 8) {
422
+ byteIdx++;
423
+ bitCount = 0;
424
+ }
425
+ }
426
+ }
427
+ ext.descriptors = descriptor;
428
+ }
429
+ }
430
+ }