@srsergio/taptapp-ar 1.0.43 → 1.0.50

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 (47) hide show
  1. package/README.md +42 -45
  2. package/dist/compiler/aframe.js +8 -8
  3. package/dist/compiler/controller.d.ts +50 -76
  4. package/dist/compiler/controller.js +72 -116
  5. package/dist/compiler/detector/detector-lite.js +82 -99
  6. package/dist/compiler/index.js +3 -3
  7. package/dist/compiler/matching/hamming-distance.d.ts +8 -0
  8. package/dist/compiler/matching/hamming-distance.js +35 -16
  9. package/dist/compiler/matching/hierarchical-clustering.d.ts +9 -0
  10. package/dist/compiler/matching/hierarchical-clustering.js +76 -56
  11. package/dist/compiler/matching/matching.js +3 -3
  12. package/dist/compiler/node-worker.js +144 -18
  13. package/dist/compiler/offline-compiler.d.ts +34 -83
  14. package/dist/compiler/offline-compiler.js +92 -96
  15. package/dist/compiler/simple-ar.d.ts +31 -57
  16. package/dist/compiler/simple-ar.js +32 -73
  17. package/dist/compiler/three.d.ts +13 -8
  18. package/dist/compiler/three.js +6 -6
  19. package/dist/compiler/tracker/extract.js +17 -14
  20. package/dist/compiler/utils/images.js +11 -16
  21. package/dist/compiler/utils/lsh-direct.d.ts +12 -0
  22. package/dist/compiler/utils/lsh-direct.js +76 -0
  23. package/dist/compiler/utils/worker-pool.js +10 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/index.js +2 -2
  26. package/dist/react/types.d.ts +1 -1
  27. package/dist/react/types.js +1 -1
  28. package/package.json +2 -1
  29. package/src/compiler/aframe.js +8 -8
  30. package/src/compiler/controller.ts +512 -0
  31. package/src/compiler/detector/detector-lite.js +87 -107
  32. package/src/compiler/index.js +3 -3
  33. package/src/compiler/matching/hamming-distance.js +39 -16
  34. package/src/compiler/matching/hierarchical-clustering.js +85 -57
  35. package/src/compiler/matching/matching.js +3 -3
  36. package/src/compiler/node-worker.js +163 -18
  37. package/src/compiler/offline-compiler.ts +513 -0
  38. package/src/compiler/{simple-ar.js → simple-ar.ts} +64 -91
  39. package/src/compiler/three.js +6 -6
  40. package/src/compiler/tracker/extract.js +18 -15
  41. package/src/compiler/utils/images.js +11 -21
  42. package/src/compiler/utils/lsh-direct.js +86 -0
  43. package/src/compiler/utils/worker-pool.js +9 -1
  44. package/src/index.ts +2 -2
  45. package/src/react/types.ts +2 -2
  46. package/src/compiler/controller.js +0 -554
  47. package/src/compiler/offline-compiler.js +0 -515
@@ -12,14 +12,14 @@
12
12
 
13
13
  import { FREAKPOINTS } from "./freak.js";
14
14
  import { gpuCompute } from "../utils/gpu-compute.js";
15
- import { binarizeFREAK32 } from "../utils/lsh-binarizer.js";
15
+ import { computeLSH64, computeFullFREAK, packLSHIntoDescriptor } from "../utils/lsh-direct.js";
16
16
 
17
- const PYRAMID_MIN_SIZE = 4; // Reducido de 8 a 4 para exprimir al máximo la resolución
17
+ const PYRAMID_MIN_SIZE = 4; // Restored to 4 for better small-scale detection
18
18
  // PYRAMID_MAX_OCTAVE ya no es necesario, el límite lo da PYRAMID_MIN_SIZE
19
19
 
20
20
 
21
- const NUM_BUCKETS_PER_DIMENSION = 8;
22
- const MAX_FEATURES_PER_BUCKET = 12; // Ajustado para un equilibrio óptimo entre densidad y estabilidad
21
+ const NUM_BUCKETS_PER_DIMENSION = 10;
22
+ const MAX_FEATURES_PER_BUCKET = 30; // Maximized to ensure robustness in Moonshot mode
23
23
 
24
24
 
25
25
  const ORIENTATION_NUM_BINS = 36;
@@ -57,7 +57,7 @@ export class DetectorLite {
57
57
  if (numOctaves === 10) break;
58
58
  }
59
59
 
60
- this.numOctaves = numOctaves;
60
+ this.numOctaves = options.maxOctaves !== undefined ? Math.min(numOctaves, options.maxOctaves) : numOctaves;
61
61
  }
62
62
 
63
63
  /**
@@ -96,16 +96,19 @@ export class DetectorLite {
96
96
  this._computeFreakDescriptors(prunedExtremas, pyramidImages);
97
97
 
98
98
  // Convertir a formato de salida
99
- const featurePoints = prunedExtremas.map(ext => ({
100
- maxima: ext.score > 0,
101
- x: ext.x * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
102
- y: ext.y * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
103
- scale: Math.pow(2, ext.octave),
104
- angle: ext.angle || 0,
105
- descriptors: (this.useLSH && ext.lsh) ? ext.lsh : (ext.descriptors || [])
106
- }));
107
-
108
- return { featurePoints };
99
+ const featurePoints = prunedExtremas.map(ext => {
100
+ const scale = Math.pow(2, ext.octave);
101
+ return {
102
+ maxima: ext.score > 0,
103
+ x: ext.x * scale + scale * 0.5 - 0.5,
104
+ y: ext.y * scale + scale * 0.5 - 0.5,
105
+ scale: scale,
106
+ angle: ext.angle || 0,
107
+ descriptors: (this.useLSH && ext.lsh) ? ext.descriptors : (ext.descriptors || [])
108
+ };
109
+ });
110
+
111
+ return { featurePoints, pyramid: pyramidImages };
109
112
  }
110
113
 
111
114
  /**
@@ -143,6 +146,10 @@ export class DetectorLite {
143
146
 
144
147
  for (let i = 0; i < this.numOctaves; i++) {
145
148
  const img1 = this._applyGaussianFilter(currentData, currentWidth, currentHeight);
149
+
150
+ // Only need the second blur if we are going to compute DoG with the NEXT layer
151
+ // or if we need it for this octave's DoG.
152
+ // Actually, for maxOctaves=1, we only need img1 and maybe img2 for one DoG layer.
146
153
  const img2 = this._applyGaussianFilter(img1.data, currentWidth, currentHeight);
147
154
 
148
155
  pyramid.push([
@@ -150,7 +157,7 @@ export class DetectorLite {
150
157
  { data: img2.data, width: currentWidth, height: currentHeight }
151
158
  ]);
152
159
 
153
- // Downsample para siguiente octava
160
+ // Downsample para siguiente octava - Only if we have more octaves to go
154
161
  if (i < this.numOctaves - 1) {
155
162
  const downsampled = this._downsample(img2.data, currentWidth, currentHeight);
156
163
  currentData = downsampled.data;
@@ -168,44 +175,47 @@ export class DetectorLite {
168
175
  _applyGaussianFilter(data, width, height) {
169
176
  const output = new Float32Array(width * height);
170
177
  const temp = new Float32Array(width * height);
171
- const k0 = 1 / 16, k1 = 4 / 16, k2 = 6 / 16;
178
+ const k0 = 0.0625, k1 = 0.25, k2 = 0.375; // 1/16, 4/16, 6/16
172
179
  const w1 = width - 1;
173
- const h1 = height - 1;
174
180
 
175
- // Horizontal pass - unrolled kernel
181
+ // Horizontal pass - Speed optimized with manual border handling
176
182
  for (let y = 0; y < height; y++) {
177
183
  const rowOffset = y * width;
178
- for (let x = 0; x < width; x++) {
179
- const x0 = x < 2 ? 0 : x - 2;
180
- const x1 = x < 1 ? 0 : x - 1;
181
- const x3 = x > w1 - 1 ? w1 : x + 1;
182
- const x4 = x > w1 - 2 ? w1 : x + 2;
183
-
184
- temp[rowOffset + x] =
185
- data[rowOffset + x0] * k0 +
186
- data[rowOffset + x1] * k1 +
187
- data[rowOffset + x] * k2 +
188
- data[rowOffset + x3] * k1 +
189
- data[rowOffset + x4] * k0;
184
+
185
+ // Left border
186
+ temp[rowOffset] = data[rowOffset] * (k0 + k1 + k2) + data[rowOffset + 1] * k1 + data[rowOffset + 2] * k0;
187
+ temp[rowOffset + 1] = data[rowOffset] * k1 + data[rowOffset + 1] * k2 + data[rowOffset + 2] * k1 + data[rowOffset + 3] * k0;
188
+
189
+ // Main loop - NO boundary checks
190
+ for (let x = 2; x < width - 2; x++) {
191
+ const pos = rowOffset + x;
192
+ temp[pos] = data[pos - 2] * k0 + data[pos - 1] * k1 + data[pos] * k2 + data[pos + 1] * k1 + data[pos + 2] * k0;
190
193
  }
194
+
195
+ // Right border
196
+ const r2 = rowOffset + width - 2;
197
+ const r1 = rowOffset + width - 1;
198
+ temp[r2] = data[r2 - 2] * k0 + data[r2 - 1] * k1 + data[r2] * k2 + data[r1] * k1;
199
+ temp[r1] = data[r1 - 2] * k0 + data[r1 - 1] * k1 + data[r1] * (k2 + k1 + k0);
191
200
  }
192
201
 
193
- // Vertical pass - unrolled kernel
194
- for (let y = 0; y < height; y++) {
195
- const y0 = (y < 2 ? 0 : y - 2) * width;
196
- const y1 = (y < 1 ? 0 : y - 1) * width;
197
- const y2 = y * width;
198
- const y3 = (y > h1 - 1 ? h1 : y + 1) * width;
199
- const y4 = (y > h1 - 2 ? h1 : y + 2) * width;
200
-
201
- for (let x = 0; x < width; x++) {
202
- output[y2 + x] =
203
- temp[y0 + x] * k0 +
204
- temp[y1 + x] * k1 +
205
- temp[y2 + x] * k2 +
206
- temp[y3 + x] * k1 +
207
- temp[y4 + x] * k0;
202
+ // Vertical pass - Speed optimized
203
+ for (let x = 0; x < width; x++) {
204
+ // Top border
205
+ output[x] = temp[x] * (k0 + k1 + k2) + temp[x + width] * k1 + temp[x + width * 2] * k0;
206
+ output[x + width] = temp[x] * k1 + temp[x + width] * k2 + temp[x + width * 2] * k1 + temp[x + width * 3] * k0;
207
+
208
+ // Main loop - NO boundary checks
209
+ for (let y = 2; y < height - 2; y++) {
210
+ const p = y * width + x;
211
+ output[p] = temp[p - width * 2] * k0 + temp[p - width] * k1 + temp[p] * k2 + temp[p + width] * k1 + temp[p + width * 2] * k0;
208
212
  }
213
+
214
+ // Bottom border
215
+ const b2 = (height - 2) * width + x;
216
+ const b1 = (height - 1) * width + x;
217
+ output[b2] = temp[b2 - width * 2] * k0 + temp[b2 - width] * k1 + temp[b2] * k2 + temp[b1] * k1;
218
+ output[b1] = temp[b1 - width * 2] * k0 + temp[b1 - width] * k1 + temp[b1] * (k2 + k1 + k0);
209
219
  }
210
220
 
211
221
  return { data: output, width, height };
@@ -215,36 +225,19 @@ export class DetectorLite {
215
225
  * Downsample imagen por factor de 2
216
226
  */
217
227
  _downsample(data, width, height) {
218
- const newWidth = Math.floor(width / 2);
219
- const newHeight = Math.floor(height / 2);
228
+ const newWidth = width >> 1;
229
+ const newHeight = height >> 1;
220
230
  const output = new Float32Array(newWidth * newHeight);
221
231
 
222
232
  for (let y = 0; y < newHeight; y++) {
233
+ const r0 = (y * 2) * width;
234
+ const r1 = r0 + width;
235
+ const dr = y * newWidth;
223
236
  for (let x = 0; x < newWidth; x++) {
224
- // Interpolación bilinear
225
- const srcX = x * 2 + 0.5;
226
- const srcY = y * 2 + 0.5;
227
- const x0 = Math.floor(srcX);
228
- const y0 = Math.floor(srcY);
229
- const x1 = Math.min(x0 + 1, width - 1);
230
- const y1 = Math.min(y0 + 1, height - 1);
231
-
232
- const fx = srcX - x0;
233
- const fy = srcY - y0;
234
-
235
- const v00 = data[y0 * width + x0];
236
- const v10 = data[y0 * width + x1];
237
- const v01 = data[y1 * width + x0];
238
- const v11 = data[y1 * width + x1];
239
-
240
- output[y * newWidth + x] =
241
- v00 * (1 - fx) * (1 - fy) +
242
- v10 * fx * (1 - fy) +
243
- v01 * (1 - fx) * fy +
244
- v11 * fx * fy;
237
+ const i2 = x * 2;
238
+ output[dr + x] = (data[r0 + i2] + data[r0 + i2 + 1] + data[r1 + i2] + data[r1 + i2 + 1]) * 0.25;
245
239
  }
246
240
  }
247
-
248
241
  return { data: output, width: newWidth, height: newHeight };
249
242
  }
250
243
 
@@ -277,21 +270,19 @@ export class DetectorLite {
277
270
  _findExtremas(dogPyramid, pyramidImages) {
278
271
  const extremas = [];
279
272
 
280
- for (let octave = 1; octave < dogPyramid.length - 1; octave++) {
273
+ for (let octave = 0; octave < dogPyramid.length; octave++) {
281
274
  const curr = dogPyramid[octave];
282
- const prev = dogPyramid[octave - 1];
283
- const next = dogPyramid[octave + 1];
275
+ const prev = octave > 0 ? dogPyramid[octave - 1] : null;
276
+ const next = octave < dogPyramid.length - 1 ? dogPyramid[octave + 1] : null;
284
277
 
285
278
  const width = curr.width;
286
279
  const height = curr.height;
287
- const prevWidth = prev.width;
288
- const nextWidth = next.width;
289
280
 
290
281
  for (let y = 1; y < height - 1; y++) {
291
282
  for (let x = 1; x < width - 1; x++) {
292
283
  const val = curr.data[y * width + x];
293
284
 
294
- if (Math.abs(val) < 0.01) continue; // Threshold reducido de 0.015 a 0.01 para mayor sensibilidad
285
+ if (Math.abs(val) < 0.003) continue; // Aggressively lowered threshold to 0.003 for max sensitivity
295
286
 
296
287
  let isMaxima = true;
297
288
  let isMinima = true;
@@ -306,10 +297,11 @@ export class DetectorLite {
306
297
  }
307
298
  }
308
299
 
309
- // Check previous scale (scaled coordinates)
310
- if (isMaxima || isMinima) {
311
- const px = Math.floor(x * 2);
312
- const py = Math.floor(y * 2);
300
+ // Check previous scale (scaled coordinates) - skip if no prev layer
301
+ if ((isMaxima || isMinima) && prev) {
302
+ const px = x << 1;
303
+ const py = y << 1;
304
+ const prevWidth = prev.width;
313
305
  for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
314
306
  for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
315
307
  const xx = Math.max(0, Math.min(prevWidth - 1, px + dx));
@@ -321,10 +313,11 @@ export class DetectorLite {
321
313
  }
322
314
  }
323
315
 
324
- // Check next scale (scaled coordinates)
325
- if (isMaxima || isMinima) {
326
- const nx = Math.floor(x / 2);
327
- const ny = Math.floor(y / 2);
316
+ // Check next scale (scaled coordinates) - skip if no next layer
317
+ if ((isMaxima || isMinima) && next) {
318
+ const nx = x >> 1;
319
+ const ny = y >> 1;
320
+ const nextWidth = next.width;
328
321
  for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
329
322
  for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
330
323
  const xx = Math.max(0, Math.min(nextWidth - 1, nx + dx));
@@ -391,7 +384,7 @@ export class DetectorLite {
391
384
  */
392
385
  _computeOrientations(extremas, pyramidImages) {
393
386
  for (const ext of extremas) {
394
- if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
387
+ if (ext.octave < 0 || ext.octave >= pyramidImages.length) {
395
388
  ext.angle = 0;
396
389
  continue;
397
390
  }
@@ -444,8 +437,8 @@ export class DetectorLite {
444
437
  */
445
438
  _computeFreakDescriptors(extremas, pyramidImages) {
446
439
  for (const ext of extremas) {
447
- if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
448
- ext.descriptors = [];
440
+ if (ext.octave < 0 || ext.octave >= pyramidImages.length) {
441
+ ext.descriptors = new Uint8Array(8);
449
442
  continue;
450
443
  }
451
444
 
@@ -479,28 +472,15 @@ export class DetectorLite {
479
472
  data[y1 * width + x1] * fracX * fracY;
480
473
  }
481
474
 
482
- // Pack pairs into Uint8Array (84 bytes per descriptor)
483
- const descriptor = new Uint8Array(84);
484
- let bitCount = 0;
485
- let byteIdx = 0;
486
-
487
- for (let i = 0; i < FREAKPOINTS.length; i++) {
488
- for (let j = i + 1; j < FREAKPOINTS.length; j++) {
489
- if (samples[i] < samples[j]) {
490
- descriptor[byteIdx] |= (1 << (7 - bitCount));
491
- }
492
- bitCount++;
493
-
494
- if (bitCount === 8) {
495
- byteIdx++;
496
- bitCount = 0;
497
- }
498
- }
499
- }
475
+ // 🚀 MOONSHOT: Direct LSH computation
476
+ // Avoids computing 672 bits of FREAK just to sample 64.
500
477
  if (this.useLSH) {
501
- ext.lsh = binarizeFREAK32(descriptor);
478
+ ext.lsh = computeLSH64(samples);
479
+ // Pack LSH into 8-byte descriptors for compatibility
480
+ ext.descriptors = packLSHIntoDescriptor(ext.lsh);
481
+ } else {
482
+ ext.descriptors = computeFullFREAK(samples);
502
483
  }
503
- ext.descriptors = descriptor;
504
484
  }
505
485
  }
506
486
  }
@@ -2,10 +2,10 @@ import { OfflineCompiler } from "./offline-compiler.js";
2
2
 
3
3
  export { OfflineCompiler };
4
4
 
5
- if (!window.MINDAR) {
6
- window.MINDAR = {};
5
+ if (!window.TAAR) {
6
+ window.TAAR = {};
7
7
  }
8
8
 
9
- window.MINDAR.IMAGE = {
9
+ window.TAAR.IMAGE = {
10
10
  OfflineCompiler,
11
11
  };
@@ -7,25 +7,51 @@ for (let i = 0; i < 256; i++) {
7
7
  }
8
8
 
9
9
  /**
10
- * Optimized popcount for 32-bit integers
10
+ * 🚀 Moonshot Optimized Popcount
11
+ * Uses a slightly faster bitwise sequence for 32-bit integers
11
12
  */
12
13
  function popcount32(n) {
13
- n = n - ((n >> 1) & 0x55555555);
14
- n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
15
- return (((n + (n >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
14
+ n = n >>> 0; // Force unsigned
15
+ n -= (n >>> 1) & 0x55555555;
16
+ n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
17
+ return (((n + (n >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
16
18
  }
17
19
 
20
+ /**
21
+ * Super-optimized Hamming distance for 64-bit LSH (2x Uint32)
22
+ * NO OBJECTS, NO OPTIONS, JUST PURE SPEED.
23
+ */
24
+ const compute64 = (v1, v1Idx, v2, v2Idx) => {
25
+ // Inline XOR and popcount for maximum speed
26
+ let x1 = (v1[v1Idx] ^ v2[v2Idx]) >>> 0;
27
+ let x2 = (v1[v1Idx + 1] ^ v2[v2Idx + 1]) >>> 0;
28
+
29
+ // Popcount 1
30
+ x1 -= (x1 >>> 1) & 0x55555555;
31
+ x1 = (x1 & 0x33333333) + ((x1 >>> 2) & 0x33333333);
32
+ const count1 = (((x1 + (x1 >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
33
+
34
+ // Popcount 2
35
+ x2 -= (x2 >>> 1) & 0x55555555;
36
+ x2 = (x2 & 0x33333333) + ((x2 >>> 2) & 0x33333333);
37
+ const count2 = (((x2 + (x2 >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
38
+
39
+ return count1 + count2;
40
+ };
41
+
42
+ /**
43
+ * Generic compute for backward compatibility
44
+ */
18
45
  const compute = (options) => {
19
46
  const { v1, v2, v1Offset = 0, v2Offset = 0 } = options;
47
+ const v2Len = v2.length - v2Offset;
20
48
 
21
- // Protocol V5 Path: 64-bit LSH (two Uint32)
22
- if (v1.length === v2.length && (v1.length / (v1.buffer.byteLength / v1.length)) === 2) {
23
- // This is a bit hacky check, better if we know the version.
24
- // Assuming if it's not 84 bytes, it's the new 8-byte format.
49
+ if (v2Len === 2) {
50
+ return compute64(v1, v1Offset, v2, v2Offset);
25
51
  }
26
52
 
27
- // If descriptors are 84 bytes (Protocol V4)
28
- if (v1.length >= v1Offset + 84 && v2.length >= v2Offset + 84 && v1[v1Offset + 83] !== undefined) {
53
+ // Protocol V4: 84-byte descriptors (Uint8Array)
54
+ if (v2Len === 84) {
29
55
  let d = 0;
30
56
  for (let i = 0; i < 84; i++) {
31
57
  d += BIT_COUNT_8[v1[v1Offset + i] ^ v2[v2Offset + i]];
@@ -33,19 +59,16 @@ const compute = (options) => {
33
59
  return d;
34
60
  }
35
61
 
36
- // Protocol V5.1 Path: LSH 128-bit (4 x 32-bit)
37
- // We expect v1 and v2 to be slices or offsets of Uint32Array
38
- if (v1.length >= v1Offset + 4 && v2.length >= v2Offset + 4 && v1[v1Offset + 3] !== undefined) {
62
+ // Protocol V5.1: 128-bit LSH (4 x Uint32)
63
+ if (v2Len === 4) {
39
64
  return popcount32(v1[v1Offset] ^ v2[v2Offset]) +
40
65
  popcount32(v1[v1Offset + 1] ^ v2[v2Offset + 1]) +
41
66
  popcount32(v1[v1Offset + 2] ^ v2[v2Offset + 2]) +
42
67
  popcount32(v1[v1Offset + 3] ^ v2[v2Offset + 3]);
43
68
  }
44
69
 
45
- // Protocol V5 Path: LSH 64-bit (2 x 32-bit)
46
- // We expect v1 and v2 to be slices or offsets of Uint32Array
47
70
  return popcount32(v1[v1Offset] ^ v2[v2Offset]) +
48
71
  popcount32(v1[v1Offset + 1] ^ v2[v2Offset + 1]);
49
72
  };
50
73
 
51
- export { compute };
74
+ export { compute, compute64 };
@@ -1,101 +1,133 @@
1
- import { compute as hammingCompute } from "./hamming-distance.js";
1
+ import { compute64 as hammingCompute64 } from "./hamming-distance.js";
2
2
  import { createRandomizer } from "../utils/randomizer.js";
3
3
 
4
- const MIN_FEATURE_PER_NODE = 16;
5
- const NUM_ASSIGNMENT_HYPOTHESES = 64;
4
+ const MIN_FEATURE_PER_NODE = 32; // Increased from 16 for speed
5
+ const NUM_ASSIGNMENT_HYPOTHESES = 12; // Reduced from 16 for speed
6
6
  const NUM_CENTERS = 8;
7
7
 
8
+ /**
9
+ * 🚀 Moonshot Optimized K-Medoids
10
+ *
11
+ * Major Optimizations:
12
+ * 1. Flattened Memory: Operates on a single Uint32Array block instead of objects.
13
+ * 2. Zero Property Access: Avoids .descriptors lookup in the tightest loop.
14
+ * 3. Cache-Friendly: Accesses contiguous descriptor data.
15
+ */
8
16
  const _computeKMedoids = (options) => {
9
- const { points, pointIndexes, randomizer } = options;
17
+ const { descriptors, pointIndexes, randomizer } = options;
18
+ const numPointIndexes = pointIndexes.length;
10
19
 
11
- const randomPointIndexes = [];
12
- for (let i = 0; i < pointIndexes.length; i++) {
13
- randomPointIndexes.push(i);
20
+ const randomPointIndexes = new Int32Array(numPointIndexes);
21
+ for (let i = 0; i < numPointIndexes; i++) {
22
+ randomPointIndexes[i] = i;
14
23
  }
15
24
 
16
25
  let bestSumD = Number.MAX_SAFE_INTEGER;
17
- let bestAssignmentIndex = -1;
26
+ let bestAssignment = null;
27
+
28
+ // Pre-fetch centers indices to avoid nested index lookups
29
+ const centerPointIndices = new Int32Array(NUM_CENTERS);
18
30
 
19
- const assignments = [];
20
31
  for (let i = 0; i < NUM_ASSIGNMENT_HYPOTHESES; i++) {
21
32
  randomizer.arrayShuffle({ arr: randomPointIndexes, sampleSize: NUM_CENTERS });
22
33
 
34
+ // Set centers for this hypothesis
35
+ for (let k = 0; k < NUM_CENTERS; k++) {
36
+ centerPointIndices[k] = pointIndexes[randomPointIndexes[k]];
37
+ }
38
+
23
39
  let sumD = 0;
24
- const assignment = [];
25
- for (let j = 0; j < pointIndexes.length; j++) {
26
- let bestD = Number.MAX_SAFE_INTEGER;
40
+ const currentAssignment = new Int32Array(numPointIndexes);
41
+
42
+ for (let j = 0; j < numPointIndexes; j++) {
43
+ const pIdx = pointIndexes[j];
44
+ const pOffset = pIdx * 2;
45
+
46
+ let bestD = 255; // Max possible Hamming for 64-bit is 64, but let's be safe
47
+ let bestCenterIdx = -1;
48
+
27
49
  for (let k = 0; k < NUM_CENTERS; k++) {
28
- const centerIndex = pointIndexes[randomPointIndexes[k]];
29
- const d = hammingCompute({
30
- v1: points[pointIndexes[j]].descriptors,
31
- v2: points[centerIndex].descriptors,
32
- });
50
+ const cIdx = centerPointIndices[k];
51
+ const cOffset = cIdx * 2;
52
+
53
+ // DIRECT CALL TO INLINED HAMMING
54
+ const d = hammingCompute64(descriptors, pOffset, descriptors, cOffset);
55
+
33
56
  if (d < bestD) {
34
- assignment[j] = randomPointIndexes[k];
57
+ bestCenterIdx = randomPointIndexes[k];
35
58
  bestD = d;
36
59
  }
37
60
  }
61
+ currentAssignment[j] = bestCenterIdx;
38
62
  sumD += bestD;
39
63
  }
40
- assignments.push(assignment);
41
64
 
42
65
  if (sumD < bestSumD) {
43
66
  bestSumD = sumD;
44
- bestAssignmentIndex = i;
67
+ bestAssignment = currentAssignment;
45
68
  }
46
69
  }
47
- return assignments[bestAssignmentIndex];
70
+ return bestAssignment;
48
71
  };
49
72
 
50
- // kmedoids clustering of points, with hamming distance of FREAK descriptor
51
- //
52
- // node = {
53
- // isLeaf: bool,
54
- // children: [], list of children node
55
- // pointIndexes: [], list of int, point indexes
56
- // centerPointIndex: int
57
- // }
73
+ /**
74
+ * Build hierarchical clusters
75
+ */
58
76
  const build = ({ points }) => {
59
- const pointIndexes = [];
60
- for (let i = 0; i < points.length; i++) {
61
- pointIndexes.push(i);
77
+ const numPoints = points.length;
78
+ if (numPoints === 0) return { rootNode: { leaf: true, pointIndexes: [], centerPointIndex: null } };
79
+
80
+ // 🚀 MOONSHOT: Flatten all descriptors into a single Uint32Array
81
+ // This is the key to sub-second performance.
82
+ const descriptors = new Uint32Array(numPoints * 2);
83
+ for (let i = 0; i < numPoints; i++) {
84
+ const d = points[i].descriptors;
85
+ descriptors[i * 2] = d[0];
86
+ descriptors[i * 2 + 1] = d[1];
87
+ }
88
+
89
+ const pointIndexes = new Int32Array(numPoints);
90
+ for (let i = 0; i < numPoints; i++) {
91
+ pointIndexes[i] = i;
62
92
  }
63
93
 
64
94
  const randomizer = createRandomizer();
65
95
 
66
96
  const rootNode = _build({
67
- points: points,
68
- pointIndexes: pointIndexes,
97
+ descriptors,
98
+ pointIndexes,
69
99
  centerPointIndex: null,
70
100
  randomizer,
71
101
  });
72
102
  return { rootNode };
73
103
  };
74
104
 
75
- // recursive build hierarchy clusters
76
105
  const _build = (options) => {
77
- const { points, pointIndexes, centerPointIndex, randomizer } = options;
106
+ const { descriptors, pointIndexes, centerPointIndex, randomizer } = options;
107
+ const numPoints = pointIndexes.length;
78
108
 
79
109
  let isLeaf = false;
80
-
81
- if (pointIndexes.length <= NUM_CENTERS || pointIndexes.length <= MIN_FEATURE_PER_NODE) {
110
+ if (numPoints <= NUM_CENTERS || numPoints <= MIN_FEATURE_PER_NODE) {
82
111
  isLeaf = true;
83
112
  }
84
113
 
85
- const clusters = {};
114
+ const clusters = new Map();
86
115
  if (!isLeaf) {
87
- // compute clusters
88
- const assignment = _computeKMedoids({ points, pointIndexes, randomizer });
116
+ const assignment = _computeKMedoids({ descriptors, pointIndexes, randomizer });
89
117
 
90
118
  for (let i = 0; i < assignment.length; i++) {
91
- if (clusters[pointIndexes[assignment[i]]] === undefined) {
92
- clusters[pointIndexes[assignment[i]]] = [];
119
+ const centerIdx = pointIndexes[assignment[i]];
120
+ let cluster = clusters.get(centerIdx);
121
+ if (cluster === undefined) {
122
+ cluster = [];
123
+ clusters.set(centerIdx, cluster);
93
124
  }
94
- clusters[pointIndexes[assignment[i]]].push(pointIndexes[i]);
125
+ cluster.push(pointIndexes[i]);
126
+ }
127
+
128
+ if (clusters.size === 1) {
129
+ isLeaf = true;
95
130
  }
96
- }
97
- if (Object.keys(clusters).length === 1) {
98
- isLeaf = true;
99
131
  }
100
132
 
101
133
  const node = {
@@ -104,27 +136,23 @@ const _build = (options) => {
104
136
 
105
137
  if (isLeaf) {
106
138
  node.leaf = true;
107
- node.pointIndexes = [];
108
- for (let i = 0; i < pointIndexes.length; i++) {
109
- node.pointIndexes.push(pointIndexes[i]);
110
- }
139
+ node.pointIndexes = new Int32Array(pointIndexes);
111
140
  return node;
112
141
  }
113
142
 
114
- // recursive build children
115
143
  node.leaf = false;
116
144
  node.children = [];
117
145
 
118
- Object.keys(clusters).forEach((centerIndex) => {
146
+ for (const [cIdx, clusterPoints] of clusters) {
119
147
  node.children.push(
120
148
  _build({
121
- points: points,
122
- pointIndexes: clusters[centerIndex],
123
- centerPointIndex: centerIndex,
149
+ descriptors,
150
+ pointIndexes: new Int32Array(clusterPoints),
151
+ centerPointIndex: cIdx,
124
152
  randomizer,
125
153
  }),
126
154
  );
127
- });
155
+ }
128
156
  return node;
129
157
  };
130
158
 
@@ -4,11 +4,11 @@ import { computeHoughMatches } from "./hough.js";
4
4
  import { computeHomography } from "./ransacHomography.js";
5
5
  import { multiplyPointHomographyInhomogenous, matrixInverse33 } from "../utils/geometry.js";
6
6
 
7
- const INLIER_THRESHOLD = 3;
7
+ const INLIER_THRESHOLD = 10; // Relaxed from 3 to 10 for better robustness with LSH
8
8
  //const MIN_NUM_INLIERS = 8; //default
9
9
  const MIN_NUM_INLIERS = 6;
10
- const CLUSTER_MAX_POP = 8;
11
- const HAMMING_THRESHOLD = 0.7;
10
+ const CLUSTER_MAX_POP = 20; // Increased to explore more candidate clusters
11
+ const HAMMING_THRESHOLD = 0.85; // Relaxed ratio test for binary descriptors
12
12
 
13
13
  // match list of querpoints against pre-built list of keyframes
14
14
  const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) => {