@srsergio/taptapp-ar 1.0.92 → 1.0.94

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 (52) hide show
  1. package/README.md +16 -14
  2. package/dist/compiler/offline-compiler.d.ts +3 -3
  3. package/dist/compiler/offline-compiler.js +50 -33
  4. package/dist/core/constants.d.ts +2 -0
  5. package/dist/core/constants.js +4 -1
  6. package/dist/core/detector/detector-lite.d.ts +6 -5
  7. package/dist/core/detector/detector-lite.js +46 -16
  8. package/dist/core/image-list.d.ts +24 -6
  9. package/dist/core/image-list.js +4 -4
  10. package/dist/core/matching/matcher.d.ts +1 -1
  11. package/dist/core/matching/matcher.js +7 -4
  12. package/dist/core/matching/matching.d.ts +2 -1
  13. package/dist/core/matching/matching.js +43 -11
  14. package/dist/core/perception/bio-inspired-engine.d.ts +130 -0
  15. package/dist/core/perception/bio-inspired-engine.js +232 -0
  16. package/dist/core/perception/foveal-attention.d.ts +142 -0
  17. package/dist/core/perception/foveal-attention.js +280 -0
  18. package/dist/core/perception/index.d.ts +6 -0
  19. package/dist/core/perception/index.js +17 -0
  20. package/dist/core/perception/predictive-coding.d.ts +92 -0
  21. package/dist/core/perception/predictive-coding.js +278 -0
  22. package/dist/core/perception/saccadic-controller.d.ts +126 -0
  23. package/dist/core/perception/saccadic-controller.js +269 -0
  24. package/dist/core/perception/saliency-map.d.ts +74 -0
  25. package/dist/core/perception/saliency-map.js +254 -0
  26. package/dist/core/perception/scale-orchestrator.d.ts +28 -0
  27. package/dist/core/perception/scale-orchestrator.js +68 -0
  28. package/dist/core/protocol.d.ts +14 -1
  29. package/dist/core/protocol.js +33 -1
  30. package/dist/runtime/bio-inspired-controller.d.ts +135 -0
  31. package/dist/runtime/bio-inspired-controller.js +358 -0
  32. package/dist/runtime/controller.d.ts +11 -2
  33. package/dist/runtime/controller.js +20 -8
  34. package/dist/runtime/controller.worker.js +2 -2
  35. package/package.json +1 -1
  36. package/src/compiler/offline-compiler.ts +56 -36
  37. package/src/core/constants.ts +5 -1
  38. package/src/core/detector/detector-lite.js +46 -16
  39. package/src/core/image-list.js +4 -4
  40. package/src/core/matching/matcher.js +8 -4
  41. package/src/core/matching/matching.js +51 -12
  42. package/src/core/perception/bio-inspired-engine.js +275 -0
  43. package/src/core/perception/foveal-attention.js +306 -0
  44. package/src/core/perception/index.js +18 -0
  45. package/src/core/perception/predictive-coding.js +327 -0
  46. package/src/core/perception/saccadic-controller.js +303 -0
  47. package/src/core/perception/saliency-map.js +296 -0
  48. package/src/core/perception/scale-orchestrator.js +80 -0
  49. package/src/core/protocol.ts +38 -1
  50. package/src/runtime/bio-inspired-controller.ts +448 -0
  51. package/src/runtime/controller.ts +22 -7
  52. package/src/runtime/controller.worker.js +2 -1
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Bio-Inspired Controller Adapter
3
+ *
4
+ * Wraps the standard Controller with Bio-Inspired Perception capabilities.
5
+ * Provides significant performance improvements while maintaining API compatibility.
6
+ *
7
+ * Key features:
8
+ * - Foveal attention: Processes only regions of interest at full resolution
9
+ * - Predictive coding: Skips processing when scene is static
10
+ * - Saccadic sampling: Strategic "glances" at high-saliency regions
11
+ *
12
+ * Usage:
13
+ * ```javascript
14
+ * import { BioInspiredController } from './bio-inspired-controller.js';
15
+ *
16
+ * const controller = new BioInspiredController({
17
+ * inputWidth: 640,
18
+ * inputHeight: 480,
19
+ * onUpdate: (data) => console.log(data),
20
+ * bioInspired: {
21
+ * enabled: true,
22
+ * aggressiveSkipping: true,
23
+ * }
24
+ * });
25
+ * ```
26
+ */
27
+ import { Controller } from './controller.js';
28
+ import { BioInspiredEngine } from '../core/perception/index.js';
29
+ /**
30
+ * Bio-Inspired Controller
31
+ *
32
+ * Extends the standard Controller with bio-inspired perception capabilities.
33
+ */
34
+ class BioInspiredController extends Controller {
35
+ bioEngine = null;
36
+ bioEnabled = true;
37
+ bioMetricsInterval = null;
38
+ lastBioResult = null;
39
+ constructor(options) {
40
+ super(options);
41
+ const bioOptions = options.bioInspired || {};
42
+ this.bioEnabled = bioOptions.enabled !== false;
43
+ if (this.bioEnabled) {
44
+ // Initialize Bio-Inspired Engine
45
+ const bioConfig = {};
46
+ if (bioOptions.foveaRadiusRatio !== undefined) {
47
+ bioConfig.FOVEA_RADIUS_RATIO = bioOptions.foveaRadiusRatio;
48
+ }
49
+ if (bioOptions.maxSaccades !== undefined) {
50
+ bioConfig.MAX_SACCADES_PER_FRAME = bioOptions.maxSaccades;
51
+ }
52
+ if (bioOptions.aggressiveSkipping !== undefined) {
53
+ bioConfig.ENABLE_SKIP_FRAMES = bioOptions.aggressiveSkipping;
54
+ if (bioOptions.aggressiveSkipping) {
55
+ bioConfig.CHANGE_THRESHOLD = 0.03; // More aggressive
56
+ }
57
+ }
58
+ this.bioEngine = new BioInspiredEngine(options.inputWidth, options.inputHeight, bioConfig);
59
+ }
60
+ }
61
+ /**
62
+ * Override processVideo to add bio-inspired perception
63
+ */
64
+ processVideo(input) {
65
+ if (!this.bioEnabled || !this.bioEngine) {
66
+ return super.processVideo(input);
67
+ }
68
+ if (this.processingVideo)
69
+ return;
70
+ this.processingVideo = true;
71
+ // Reset tracking states
72
+ this.trackingStates = [];
73
+ for (let i = 0; i < (this.markerDimensions?.length || 0); i++) {
74
+ this.trackingStates.push({
75
+ showing: false,
76
+ isTracking: false,
77
+ currentModelViewTransform: null,
78
+ trackCount: 0,
79
+ trackMiss: 0,
80
+ });
81
+ }
82
+ const startProcessing = async () => {
83
+ while (this.processingVideo) {
84
+ const inputData = this.inputLoader.loadInput(input);
85
+ // Get current tracking state for bio engine
86
+ const activeTracking = this.trackingStates.find(s => s.isTracking);
87
+ const trackingState = activeTracking ? {
88
+ isTracking: true,
89
+ activeOctave: activeTracking.lastOctaveIndex, // Tracked octave index
90
+ worldMatrix: activeTracking.currentModelViewTransform
91
+ ? this._flattenMatrix(activeTracking.currentModelViewTransform)
92
+ : null
93
+ } : null;
94
+ // Process through bio-inspired engine
95
+ const bioResult = this.bioEngine.process(inputData, trackingState || undefined);
96
+ this.lastBioResult = bioResult;
97
+ // If bio engine says we can skip, use prediction
98
+ if (bioResult.skipped && activeTracking?.isTracking) {
99
+ // Use predicted state
100
+ this._handleSkippedFrame(activeTracking, bioResult);
101
+ }
102
+ else {
103
+ // Normal processing with attention regions
104
+ await this._processWithAttention(input, inputData, bioResult);
105
+ }
106
+ // Wait for next frame
107
+ if (typeof requestAnimationFrame !== 'undefined') {
108
+ await new Promise(requestAnimationFrame);
109
+ }
110
+ else {
111
+ await new Promise(resolve => setTimeout(resolve, 16));
112
+ }
113
+ }
114
+ };
115
+ startProcessing();
116
+ }
117
+ /**
118
+ * Handle a skipped frame using prediction
119
+ * @private
120
+ */
121
+ _handleSkippedFrame(trackingState, bioResult) {
122
+ // Use predicted matrix
123
+ if (bioResult.prediction && bioResult.prediction.worldMatrix) {
124
+ trackingState.currentModelViewTransform = this._unflattenMatrix(bioResult.prediction.worldMatrix);
125
+ }
126
+ // Notify with skipped status
127
+ const worldMatrix = trackingState.currentModelViewTransform
128
+ ? this._glModelViewMatrix(trackingState.currentModelViewTransform, 0)
129
+ : null;
130
+ this.onUpdate?.({
131
+ type: 'updateMatrix',
132
+ targetIndex: 0,
133
+ worldMatrix: worldMatrix ? this.featureManager.applyWorldMatrixFilters(0, worldMatrix, { stability: 0.9 }) : null,
134
+ skipped: true,
135
+ bioMetrics: this.bioEngine?.getMetrics(),
136
+ });
137
+ this.onUpdate?.({ type: 'processDone' });
138
+ }
139
+ /**
140
+ * Process frame using bio-inspired attention regions
141
+ * @private
142
+ */
143
+ async _processWithAttention(input, inputData, bioResult) {
144
+ const nTracking = this.trackingStates.reduce((acc, s) => acc + (s.isTracking ? 1 : 0), 0);
145
+ // Detection phase - use primary attention region for efficiency
146
+ if (nTracking < this.maxTrack) {
147
+ const matchingIndexes = this.trackingStates
148
+ .map((s, i) => ({ state: s, index: i }))
149
+ .filter(({ state, index }) => !state.isTracking &&
150
+ (this.interestedTargetIndex === -1 || this.interestedTargetIndex === index))
151
+ .map(({ index }) => index);
152
+ if (matchingIndexes.length > 0) {
153
+ // Use full input for detection (bio engine already optimized upstream processing)
154
+ const { targetIndex: matchedTargetIndex, modelViewTransform, featurePoints } = await this._detectAndMatch(inputData, matchingIndexes, bioResult.octavesToProcess || null);
155
+ if (matchedTargetIndex !== -1) {
156
+ this.trackingStates[matchedTargetIndex].isTracking = true;
157
+ this.trackingStates[matchedTargetIndex].currentModelViewTransform = modelViewTransform;
158
+ // Update bio engine fovea to focus on detected target
159
+ if (bioResult.attentionRegions?.[0]) {
160
+ this.bioEngine?.reset();
161
+ }
162
+ }
163
+ this.onUpdate?.({ type: 'featurePoints', featurePoints });
164
+ }
165
+ }
166
+ // Tracking phase
167
+ for (let i = 0; i < this.trackingStates.length; i++) {
168
+ const trackingState = this.trackingStates[i];
169
+ if (trackingState.isTracking) {
170
+ const result = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
171
+ if (!result || !result.modelViewTransform) {
172
+ trackingState.isTracking = false;
173
+ trackingState.screenCoords = result?.screenCoords || [];
174
+ trackingState.reliabilities = result?.reliabilities || [];
175
+ trackingState.stabilities = result?.stabilities || [];
176
+ }
177
+ else {
178
+ trackingState.currentModelViewTransform = result.modelViewTransform;
179
+ trackingState.screenCoords = result.screenCoords;
180
+ trackingState.reliabilities = result.reliabilities;
181
+ trackingState.stabilities = result.stabilities;
182
+ trackingState.deformedMesh = result.deformedMesh;
183
+ }
184
+ }
185
+ const wasShowing = trackingState.showing;
186
+ trackingState.showing = this.featureManager.shouldShow(i, trackingState.isTracking);
187
+ if (wasShowing && !trackingState.showing) {
188
+ trackingState.trackingMatrix = null;
189
+ this.featureManager.notifyUpdate({ type: 'reset', targetIndex: i });
190
+ }
191
+ // Emit update
192
+ if (trackingState.showing || trackingState.screenCoords?.length > 0 || (wasShowing && !trackingState.showing)) {
193
+ const worldMatrix = trackingState.showing
194
+ ? this._glModelViewMatrix(trackingState.currentModelViewTransform, i)
195
+ : null;
196
+ let finalMatrix = null;
197
+ if (worldMatrix) {
198
+ const stabilities = trackingState.stabilities || [];
199
+ const avgStability = stabilities.length > 0
200
+ ? stabilities.reduce((a, b) => a + b, 0) / stabilities.length
201
+ : 0;
202
+ finalMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
203
+ trackingState.trackingMatrix = finalMatrix;
204
+ const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
205
+ if (isInputRotated) {
206
+ const rotationFeature = this.featureManager.getFeature('auto-rotation');
207
+ if (rotationFeature) {
208
+ finalMatrix = rotationFeature.rotate(finalMatrix);
209
+ }
210
+ }
211
+ }
212
+ this.onUpdate?.({
213
+ type: 'updateMatrix',
214
+ targetIndex: i,
215
+ worldMatrix: finalMatrix,
216
+ modelViewTransform: trackingState.currentModelViewTransform,
217
+ screenCoords: trackingState.screenCoords,
218
+ reliabilities: trackingState.reliabilities,
219
+ stabilities: trackingState.stabilities,
220
+ deformedMesh: trackingState.deformedMesh,
221
+ bioMetrics: this.bioEngine?.getMetrics(),
222
+ foveaCenter: bioResult.foveaCenter,
223
+ pixelsSaved: bioResult.pixelsSaved,
224
+ });
225
+ }
226
+ }
227
+ this.onUpdate?.({ type: 'processDone' });
228
+ }
229
+ /**
230
+ * Detect and match features, optionally limited to specific octaves
231
+ */
232
+ async _detectAndMatch(inputData, targetIndexes, octavesToProcess = null) {
233
+ // 🚀 NANITE-STYLE: Estimate scale for filtered matching
234
+ let predictedScale = undefined;
235
+ for (const state of this.trackingStates) {
236
+ if (state.isTracking && state.currentModelViewTransform) {
237
+ const m = state.currentModelViewTransform;
238
+ predictedScale = Math.sqrt(m[0][0] ** 2 + m[1][0] ** 2 + m[2][0] ** 2);
239
+ break;
240
+ }
241
+ }
242
+ const { targetIndex, modelViewTransform, screenCoords, worldCoords, featurePoints } = await this._workerMatch(null, // No feature points, worker will detect from inputData
243
+ targetIndexes, inputData, predictedScale, octavesToProcess);
244
+ return { targetIndex, modelViewTransform, screenCoords, worldCoords, featurePoints };
245
+ }
246
+ /**
247
+ * Communicate with worker for matching phase
248
+ */
249
+ _workerMatch(featurePoints, targetIndexes, inputData = null, expectedScale, octavesToProcess = null) {
250
+ return new Promise((resolve) => {
251
+ if (!this.worker) {
252
+ // If no feature points but we have input data, detect first
253
+ let fpPromise;
254
+ if (!featurePoints && inputData) {
255
+ fpPromise = Promise.resolve(this.fullDetector.detect(inputData, { octavesToProcess }).featurePoints);
256
+ }
257
+ else {
258
+ fpPromise = Promise.resolve(featurePoints);
259
+ }
260
+ fpPromise.then(fp => {
261
+ this._matchOnMainThread(fp, targetIndexes, expectedScale).then(resolve);
262
+ }).catch(() => resolve({ targetIndex: -1 }));
263
+ return;
264
+ }
265
+ const timeout = setTimeout(() => {
266
+ this.workerMatchDone = null;
267
+ resolve({ targetIndex: -1 });
268
+ }, 1000);
269
+ this.workerMatchDone = (data) => {
270
+ clearTimeout(timeout);
271
+ this.workerMatchDone = null;
272
+ resolve(data);
273
+ };
274
+ if (inputData) {
275
+ this.worker.postMessage({ type: "match", inputData, targetIndexes, octavesToProcess, expectedScale });
276
+ }
277
+ else {
278
+ this.worker.postMessage({ type: "match", featurePoints: featurePoints, targetIndexes, expectedScale });
279
+ }
280
+ });
281
+ }
282
+ /**
283
+ * Override _trackAndUpdate to capture active octave for the next frame's orchestration
284
+ */
285
+ async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
286
+ const result = await super._trackAndUpdate(inputData, lastModelViewTransform, targetIndex);
287
+ if (result && result.octaveIndex !== undefined) {
288
+ this.trackingStates[targetIndex].lastOctaveIndex = result.octaveIndex;
289
+ }
290
+ return result;
291
+ }
292
+ /**
293
+ * Flatten a 3x4 matrix to Float32Array
294
+ * @private
295
+ */
296
+ _flattenMatrix(matrix) {
297
+ const result = new Float32Array(16);
298
+ for (let i = 0; i < 3; i++) {
299
+ for (let j = 0; j < 4; j++) {
300
+ result[i * 4 + j] = matrix[i][j];
301
+ }
302
+ }
303
+ result[12] = 0;
304
+ result[13] = 0;
305
+ result[14] = 0;
306
+ result[15] = 1;
307
+ return result;
308
+ }
309
+ /**
310
+ * Unflatten Float32Array to 3x4 matrix
311
+ * @private
312
+ */
313
+ _unflattenMatrix(flat) {
314
+ return [
315
+ [flat[0], flat[1], flat[2], flat[3]],
316
+ [flat[4], flat[5], flat[6], flat[7]],
317
+ [flat[8], flat[9], flat[10], flat[11]],
318
+ ];
319
+ }
320
+ /**
321
+ * Get bio-inspired engine metrics
322
+ */
323
+ getBioMetrics() {
324
+ return this.bioEngine?.getMetrics() || null;
325
+ }
326
+ /**
327
+ * Get last bio processing result
328
+ */
329
+ getLastBioResult() {
330
+ return this.lastBioResult;
331
+ }
332
+ /**
333
+ * Enable/disable bio-inspired processing dynamically
334
+ */
335
+ setBioEnabled(enabled) {
336
+ this.bioEnabled = enabled;
337
+ if (enabled && !this.bioEngine) {
338
+ this.bioEngine = new BioInspiredEngine(this.inputWidth, this.inputHeight);
339
+ }
340
+ }
341
+ /**
342
+ * Configure bio-inspired engine at runtime
343
+ */
344
+ configureBio(options) {
345
+ this.bioEngine?.configure(options);
346
+ }
347
+ /**
348
+ * Override dispose to clean up bio engine
349
+ */
350
+ dispose() {
351
+ super.dispose();
352
+ this.bioEngine = null;
353
+ if (this.bioMetricsInterval) {
354
+ clearInterval(this.bioMetricsInterval);
355
+ }
356
+ }
357
+ }
358
+ export { BioInspiredController };
@@ -68,17 +68,26 @@ declare class Controller {
68
68
  featurePoints: any;
69
69
  }>;
70
70
  _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<{
71
+ modelViewTransform: null;
72
+ screenCoords: never[];
73
+ reliabilities: never[];
74
+ stabilities: never[];
75
+ deformedMesh: null;
76
+ octaveIndex?: undefined;
77
+ } | {
71
78
  modelViewTransform: null;
72
79
  screenCoords: any[];
73
80
  reliabilities: number[];
74
81
  stabilities: number[];
75
82
  deformedMesh?: undefined;
83
+ octaveIndex?: undefined;
76
84
  } | {
77
85
  modelViewTransform: any;
78
86
  screenCoords: any[];
79
87
  reliabilities: number[];
80
88
  stabilities: number[];
81
89
  deformedMesh: any;
90
+ octaveIndex: any;
82
91
  }>;
83
92
  processVideo(input: any): void;
84
93
  stopProcessVideo(): void;
@@ -121,9 +130,9 @@ declare class Controller {
121
130
  debugExtra: {};
122
131
  }>;
123
132
  trackUpdate(modelViewTransform: number[][], trackFeatures: any): Promise<any>;
124
- _workerMatch(featurePoints: any, targetIndexes: number[], inputData?: any): Promise<any>;
133
+ _workerMatch(featurePoints: any, targetIndexes: number[], inputData?: any, expectedScale?: number): Promise<any>;
125
134
  _workerTrack(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<any>;
126
- _matchOnMainThread(featurePoints: any, targetIndexes: number[]): Promise<{
135
+ _matchOnMainThread(featurePoints: any, targetIndexes: number[], expectedScale?: number): Promise<{
127
136
  targetIndex: number;
128
137
  modelViewTransform: any;
129
138
  screenCoords: any;
@@ -192,8 +192,19 @@ class Controller {
192
192
  return this._glModelViewMatrix(modelViewTransform, targetIndex);
193
193
  }
194
194
  async _detectAndMatch(inputData, targetIndexes) {
195
+ // 🚀 NANITE-STYLE: Estimate scale for filtered matching
196
+ // If we were already tracking a target, use its scale as a hint for faster matching
197
+ let predictedScale = undefined;
198
+ for (const state of this.trackingStates) {
199
+ if (state.isTracking && state.currentModelViewTransform) {
200
+ const m = state.currentModelViewTransform;
201
+ // Vector magnitude of the first column is a good approximation of the scale
202
+ predictedScale = Math.sqrt(m[0][0] ** 2 + m[1][0] ** 2 + m[2][0] ** 2);
203
+ break;
204
+ }
205
+ }
195
206
  const { targetIndex, modelViewTransform, screenCoords, worldCoords, featurePoints } = await this._workerMatch(null, // No feature points, worker will detect from inputData
196
- targetIndexes, inputData);
207
+ targetIndexes, inputData, predictedScale);
197
208
  return { targetIndex, modelViewTransform, screenCoords, worldCoords, featurePoints };
198
209
  }
199
210
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
@@ -279,7 +290,8 @@ class Controller {
279
290
  screenCoords: finalScreenCoords,
280
291
  reliabilities: finalReliabilities,
281
292
  stabilities: finalStabilities,
282
- deformedMesh
293
+ deformedMesh,
294
+ octaveIndex // Pass this up for the orchestrator
283
295
  };
284
296
  }
285
297
  processVideo(input) {
@@ -414,7 +426,7 @@ class Controller {
414
426
  return null;
415
427
  return this._workerTrackUpdate(modelViewTransform, trackFeatures);
416
428
  }
417
- _workerMatch(featurePoints, targetIndexes, inputData = null) {
429
+ _workerMatch(featurePoints, targetIndexes, inputData = null, expectedScale) {
418
430
  return new Promise((resolve) => {
419
431
  if (!this.worker) {
420
432
  // If no feature points but we have input data, detect first
@@ -426,7 +438,7 @@ class Controller {
426
438
  fpPromise = Promise.resolve(featurePoints);
427
439
  }
428
440
  fpPromise.then(fp => {
429
- this._matchOnMainThread(fp, targetIndexes).then(resolve);
441
+ this._matchOnMainThread(fp, targetIndexes, expectedScale).then(resolve);
430
442
  }).catch(() => resolve({ targetIndex: -1 }));
431
443
  return;
432
444
  }
@@ -447,10 +459,10 @@ class Controller {
447
459
  });
448
460
  };
449
461
  if (inputData) {
450
- this.worker.postMessage({ type: "match", inputData, targetIndexes });
462
+ this.worker.postMessage({ type: "match", inputData, targetIndexes, expectedScale });
451
463
  }
452
464
  else {
453
- this.worker.postMessage({ type: "match", featurePoints: featurePoints, targetIndexes });
465
+ this.worker.postMessage({ type: "match", featurePoints: featurePoints, targetIndexes, expectedScale });
454
466
  }
455
467
  });
456
468
  }
@@ -477,7 +489,7 @@ class Controller {
477
489
  });
478
490
  });
479
491
  }
480
- async _matchOnMainThread(featurePoints, targetIndexes) {
492
+ async _matchOnMainThread(featurePoints, targetIndexes, expectedScale) {
481
493
  if (!this.mainThreadMatcher) {
482
494
  const { Matcher } = await import("../core/matching/matcher.js");
483
495
  const { Estimator } = await import("../core/estimation/estimator.js");
@@ -491,7 +503,7 @@ class Controller {
491
503
  let matchedDebugExtra = null;
492
504
  for (let i = 0; i < targetIndexes.length; i++) {
493
505
  const matchingIndex = targetIndexes[i];
494
- const { keyframeIndex, screenCoords, worldCoords, debugExtra } = this.mainThreadMatcher.matchDetection(this.matchingDataList[matchingIndex], featurePoints);
506
+ const { keyframeIndex, screenCoords, worldCoords, debugExtra } = this.mainThreadMatcher.matchDetection(this.matchingDataList[matchingIndex], featurePoints, expectedScale);
495
507
  matchedDebugExtra = debugExtra;
496
508
  if (keyframeIndex !== -1) {
497
509
  const modelViewTransform = this.mainThreadEstimator.estimate({ screenCoords, worldCoords });
@@ -34,12 +34,12 @@ onmessage = (msg) => {
34
34
  // New: If the worker received image data, run detector here too
35
35
  let featurePoints = data.featurePoints;
36
36
  if (data.inputData) {
37
- const detectionResult = detector.detect(data.inputData);
37
+ const detectionResult = detector.detect(data.inputData, { octavesToProcess: data.octavesToProcess });
38
38
  featurePoints = detectionResult.featurePoints;
39
39
  }
40
40
  for (let i = 0; i < interestedTargetIndexes.length; i++) {
41
41
  const matchingIndex = interestedTargetIndexes[i];
42
- const { keyframeIndex, screenCoords, worldCoords, debugExtra } = matcher.matchDetection(matchingDataList[matchingIndex], featurePoints);
42
+ const { keyframeIndex, screenCoords, worldCoords, debugExtra } = matcher.matchDetection(matchingDataList[matchingIndex], featurePoints, data.expectedScale);
43
43
  matchedDebugExtra = debugExtra;
44
44
  if (keyframeIndex !== -1) {
45
45
  const modelViewTransform = estimator.estimate({ screenCoords, worldCoords });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.92",
3
+ "version": "1.0.94",
4
4
  "description": "Ultra-fast Augmented Reality (AR) SDK for Node.js and Browser. Image tracking with 100% pure JavaScript, zero-dependencies, and high-performance compilation.",
5
5
  "keywords": [
6
6
  "augmented reality",
@@ -94,36 +94,51 @@ export class OfflineCompiler {
94
94
  const results = [];
95
95
  for (let i = 0; i < targetImages.length; i++) {
96
96
  const targetImage = targetImages[i];
97
- const fullImageList = buildImageList(targetImage);
98
- // 🚀 MOONSHOT: Keep many scales for better robustness
99
- const imageList = fullImageList;
100
- const percentPerImageScale = percentPerImage / imageList.length;
101
-
102
- const keyframes = [];
103
-
104
- for (const image of imageList as any[]) {
105
- const detector = new DetectorLite(image.width, image.height, { useLSH: AR_CONFIG.USE_LSH, maxFeaturesPerBucket: AR_CONFIG.MAX_FEATURES_PER_BUCKET });
106
- const { featurePoints: ps } = detector.detect(image.data);
107
-
108
- const maximaPoints = ps.filter((p: any) => p.maxima);
109
- const minimaPoints = ps.filter((p: any) => !p.maxima);
110
- const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
111
- const minimaPointsCluster = hierarchicalClusteringBuild({ points: minimaPoints });
112
-
113
- keyframes.push({
114
- maximaPoints,
115
- minimaPoints,
116
- maximaPointsCluster,
117
- minimaPointsCluster,
118
- width: image.width,
119
- height: image.height,
120
- scale: image.scale,
121
- });
122
- currentPercent += percentPerImageScale;
123
- progressCallback(currentPercent);
97
+
98
+ // 🚀 NANITE-STYLE: Only process the target at scale 1.0
99
+ // The DetectorLite already builds its own pyramid and finds features at all octaves (virtualized LOD)
100
+ const detector = new DetectorLite(targetImage.width, targetImage.height, {
101
+ useLSH: AR_CONFIG.USE_LSH,
102
+ maxFeaturesPerBucket: AR_CONFIG.MAX_FEATURES_PER_BUCKET
103
+ });
104
+ const { featurePoints: rawPs } = detector.detect(targetImage.data);
105
+
106
+ // 🎯 Stratified Sampling: Ensure we have features from ALL scales
107
+ // We take the top N features per octave to guarantee scale coverage (Nanite-style)
108
+ const octaves = [0, 1, 2, 3, 4, 5];
109
+ const ps: any[] = [];
110
+ const featuresPerOctave = 300;
111
+
112
+ for (const oct of octaves) {
113
+ const octScale = Math.pow(2, oct);
114
+ const octFeatures = rawPs
115
+ .filter(p => Math.abs(p.scale - octScale) < 0.1)
116
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
117
+ .slice(0, featuresPerOctave);
118
+ ps.push(...octFeatures);
124
119
  }
125
120
 
126
- results.push(keyframes);
121
+ const maximaPoints = ps.filter((p: any) => p.maxima);
122
+ const minimaPoints = ps.filter((p: any) => !p.maxima);
123
+ const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
124
+ const minimaPointsCluster = hierarchicalClusteringBuild({ points: minimaPoints });
125
+
126
+ const keyframe = {
127
+ maximaPoints,
128
+ minimaPoints,
129
+ maximaPointsCluster,
130
+ minimaPointsCluster,
131
+ width: targetImage.width,
132
+ height: targetImage.height,
133
+ scale: 1.0,
134
+ };
135
+
136
+ // Wrapped in array because the protocol expects matchingData to be an array of keyframes
137
+ // We provide only one keyframe containing features from all octaves
138
+ results.push([keyframe]);
139
+
140
+ currentPercent += percentPerImage;
141
+ progressCallback(currentPercent);
127
142
  }
128
143
 
129
144
  return results;
@@ -204,14 +219,19 @@ export class OfflineCompiler {
204
219
  }
205
220
  };
206
221
  }),
207
- matchingData: item.matchingData.map((kf: any) => ({
208
- w: kf.width,
209
- h: kf.height,
210
- s: kf.scale,
211
- hdc: false,
212
- max: protocol.columnarize(kf.maximaPoints, kf.maximaPointsCluster, kf.width, kf.height, false),
213
- min: protocol.columnarize(kf.minimaPoints, kf.minimaPointsCluster, kf.width, kf.height, false),
214
- })),
222
+ matchingData: item.matchingData.map((kf: any) => {
223
+ const useCompact = AR_CONFIG.USE_COMPACT_DESCRIPTORS;
224
+ const columnarizeFn = useCompact ? protocol.columnarizeCompact : protocol.columnarize;
225
+ return {
226
+ w: kf.width,
227
+ h: kf.height,
228
+ s: kf.scale,
229
+ hdc: false,
230
+ max: columnarizeFn(kf.maximaPoints, kf.maximaPointsCluster, kf.width, kf.height),
231
+ min: columnarizeFn(kf.minimaPoints, kf.minimaPointsCluster, kf.width, kf.height),
232
+ };
233
+ }),
234
+
215
235
  };
216
236
  });
217
237
 
@@ -29,7 +29,7 @@ export const AR_CONFIG = {
29
29
 
30
30
  // Image processing / Scale list
31
31
  MIN_IMAGE_PIXEL_SIZE: 32,
32
- SCALE_STEP_EXPONENT: 0.6,
32
+ SCALE_STEP_EXPONENT: 1.0, // Optimized: was 0.6, now 1.0 (reduces scales from ~7 to ~4)
33
33
  TRACKING_DOWNSCALE_LEVEL_1: 256.0,
34
34
  TRACKING_DOWNSCALE_LEVEL_2: 128.0,
35
35
 
@@ -38,4 +38,8 @@ export const AR_CONFIG = {
38
38
  MISS_TOLERANCE: 1,
39
39
  ONE_EURO_FILTER_CUTOFF: 0.5,
40
40
  ONE_EURO_FILTER_BETA: 0.1,
41
+
42
+ // TAAR Size Optimization
43
+ USE_COMPACT_DESCRIPTORS: true, // 32-bit XOR folded descriptors vs 64-bit raw
44
+ COMPACT_HAMMING_THRESHOLD: 8, // Threshold for 32-bit descriptors (vs 15 for 64-bit)
41
45
  };