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