@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.
- package/README.md +16 -14
- package/dist/compiler/offline-compiler.d.ts +3 -3
- package/dist/compiler/offline-compiler.js +50 -33
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +4 -1
- package/dist/core/detector/detector-lite.d.ts +6 -5
- package/dist/core/detector/detector-lite.js +46 -16
- package/dist/core/image-list.d.ts +24 -6
- package/dist/core/image-list.js +4 -4
- package/dist/core/matching/matcher.d.ts +1 -1
- package/dist/core/matching/matcher.js +7 -4
- package/dist/core/matching/matching.d.ts +2 -1
- package/dist/core/matching/matching.js +43 -11
- package/dist/core/perception/bio-inspired-engine.d.ts +130 -0
- package/dist/core/perception/bio-inspired-engine.js +232 -0
- package/dist/core/perception/foveal-attention.d.ts +142 -0
- package/dist/core/perception/foveal-attention.js +280 -0
- package/dist/core/perception/index.d.ts +6 -0
- package/dist/core/perception/index.js +17 -0
- package/dist/core/perception/predictive-coding.d.ts +92 -0
- package/dist/core/perception/predictive-coding.js +278 -0
- package/dist/core/perception/saccadic-controller.d.ts +126 -0
- package/dist/core/perception/saccadic-controller.js +269 -0
- package/dist/core/perception/saliency-map.d.ts +74 -0
- package/dist/core/perception/saliency-map.js +254 -0
- package/dist/core/perception/scale-orchestrator.d.ts +28 -0
- package/dist/core/perception/scale-orchestrator.js +68 -0
- package/dist/core/protocol.d.ts +14 -1
- package/dist/core/protocol.js +33 -1
- package/dist/runtime/bio-inspired-controller.d.ts +135 -0
- package/dist/runtime/bio-inspired-controller.js +358 -0
- package/dist/runtime/controller.d.ts +11 -2
- package/dist/runtime/controller.js +20 -8
- package/dist/runtime/controller.worker.js +2 -2
- package/package.json +1 -1
- package/src/compiler/offline-compiler.ts +56 -36
- package/src/core/constants.ts +5 -1
- package/src/core/detector/detector-lite.js +46 -16
- package/src/core/image-list.js +4 -4
- package/src/core/matching/matcher.js +8 -4
- package/src/core/matching/matching.js +51 -12
- package/src/core/perception/bio-inspired-engine.js +275 -0
- package/src/core/perception/foveal-attention.js +306 -0
- package/src/core/perception/index.js +18 -0
- package/src/core/perception/predictive-coding.js +327 -0
- package/src/core/perception/saccadic-controller.js +303 -0
- package/src/core/perception/saliency-map.js +296 -0
- package/src/core/perception/scale-orchestrator.js +80 -0
- package/src/core/protocol.ts +38 -1
- package/src/runtime/bio-inspired-controller.ts +448 -0
- package/src/runtime/controller.ts +22 -7
- 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.
|
|
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
|
-
|
|
98
|
-
// 🚀
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
package/src/core/constants.ts
CHANGED
|
@@ -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
|
};
|