@srsergio/taptapp-ar 1.0.78 → 1.0.80
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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/compiler/controller.d.ts +22 -6
- package/dist/compiler/controller.js +99 -26
- package/dist/compiler/controller.worker.js +2 -1
- package/dist/compiler/estimation/refine-estimate.d.ts +2 -1
- package/dist/compiler/estimation/refine-estimate.js +18 -5
- package/dist/compiler/features/feature-base.d.ts +1 -1
- package/dist/compiler/features/feature-manager.d.ts +1 -1
- package/dist/compiler/features/feature-manager.js +2 -2
- package/dist/compiler/features/one-euro-filter-feature.d.ts +1 -1
- package/dist/compiler/features/one-euro-filter-feature.js +8 -1
- package/dist/compiler/matching/matching.js +1 -1
- package/dist/compiler/offline-compiler.d.ts +92 -8
- package/dist/compiler/offline-compiler.js +3 -86
- package/dist/compiler/simple-ar.d.ts +12 -0
- package/dist/compiler/simple-ar.js +46 -19
- package/dist/compiler/tracker/tracker.d.ts +10 -0
- package/dist/compiler/tracker/tracker.js +6 -4
- package/dist/react/TaptappAR.js +29 -2
- package/dist/react/use-ar.d.ts +7 -0
- package/dist/react/use-ar.js +16 -1
- package/package.json +24 -2
- package/src/compiler/controller.ts +112 -26
- package/src/compiler/controller.worker.js +2 -1
- package/src/compiler/estimation/refine-estimate.js +20 -3
- package/src/compiler/features/feature-base.ts +1 -1
- package/src/compiler/features/feature-manager.ts +2 -2
- package/src/compiler/features/one-euro-filter-feature.ts +11 -1
- package/src/compiler/matching/matching.js +1 -1
- package/src/compiler/offline-compiler.ts +3 -94
- package/src/compiler/simple-ar.ts +62 -20
- package/src/compiler/tracker/tracker.js +7 -4
- package/src/react/TaptappAR.tsx +41 -1
- package/src/react/use-ar.ts +24 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Tracker } from "./tracker/tracker.js";
|
|
2
|
-
import { CropDetector } from "./detector/crop-detector.js";
|
|
3
2
|
import { OfflineCompiler as Compiler } from "./offline-compiler.js";
|
|
4
3
|
import { InputLoader } from "./input-loader.js";
|
|
5
4
|
import { FeatureManager } from "./features/feature-manager.js";
|
|
6
5
|
import { OneEuroFilterFeature } from "./features/one-euro-filter-feature.js";
|
|
7
6
|
import { TemporalFilterFeature } from "./features/temporal-filter-feature.js";
|
|
8
7
|
import { AutoRotationFeature } from "./features/auto-rotation-feature.js";
|
|
9
|
-
import {
|
|
8
|
+
import { DetectorLite } from "./detector/detector-lite.js";
|
|
10
9
|
|
|
11
10
|
let ControllerWorker: any;
|
|
12
11
|
|
|
@@ -26,7 +25,10 @@ ControllerWorker = await getControllerWorker();
|
|
|
26
25
|
const DEFAULT_FILTER_CUTOFF = 0.5;
|
|
27
26
|
const DEFAULT_FILTER_BETA = 0.1;
|
|
28
27
|
const DEFAULT_WARMUP_TOLERANCE = 2; // Instant detection
|
|
29
|
-
const DEFAULT_MISS_TOLERANCE =
|
|
28
|
+
const DEFAULT_MISS_TOLERANCE = 1; // Immediate response to tracking loss
|
|
29
|
+
const WORKER_TIMEOUT_MS = 1000; // Prevent worker hangs from killing the loop
|
|
30
|
+
|
|
31
|
+
let loopIdCounter = 0;
|
|
30
32
|
|
|
31
33
|
export interface ControllerOptions {
|
|
32
34
|
inputWidth: number;
|
|
@@ -62,6 +64,7 @@ class Controller {
|
|
|
62
64
|
mainThreadMatcher: any;
|
|
63
65
|
mainThreadEstimator: any;
|
|
64
66
|
featureManager: FeatureManager;
|
|
67
|
+
fullDetector: DetectorLite | null = null;
|
|
65
68
|
|
|
66
69
|
constructor({
|
|
67
70
|
inputWidth,
|
|
@@ -89,7 +92,7 @@ class Controller {
|
|
|
89
92
|
missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance
|
|
90
93
|
));
|
|
91
94
|
this.featureManager.addFeature(new AutoRotationFeature());
|
|
92
|
-
|
|
95
|
+
// User wants "sin recortes", so we don't add CropDetectionFeature
|
|
93
96
|
|
|
94
97
|
this.inputLoader = new InputLoader(this.inputWidth, this.inputHeight);
|
|
95
98
|
this.onUpdate = onUpdate;
|
|
@@ -97,6 +100,9 @@ class Controller {
|
|
|
97
100
|
this.worker = worker;
|
|
98
101
|
if (this.worker) this._setupWorkerListener();
|
|
99
102
|
|
|
103
|
+
// Moonshot: Full frame detector for better sensitivity
|
|
104
|
+
this.fullDetector = new DetectorLite(this.inputWidth, this.inputHeight, { useLSH: true });
|
|
105
|
+
|
|
100
106
|
this.featureManager.init({
|
|
101
107
|
inputWidth: this.inputWidth,
|
|
102
108
|
inputHeight: this.inputHeight,
|
|
@@ -219,8 +225,7 @@ class Controller {
|
|
|
219
225
|
|
|
220
226
|
dummyRun(input: any) {
|
|
221
227
|
const inputData = this.inputLoader.loadInput(input);
|
|
222
|
-
|
|
223
|
-
cropFeature?.detect(inputData, false);
|
|
228
|
+
this.fullDetector?.detect(inputData);
|
|
224
229
|
this.tracker!.dummyRun(inputData);
|
|
225
230
|
}
|
|
226
231
|
|
|
@@ -242,8 +247,7 @@ class Controller {
|
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
async _detectAndMatch(inputData: any, targetIndexes: number[]) {
|
|
245
|
-
const
|
|
246
|
-
const { featurePoints } = cropFeature!.detect(inputData, true);
|
|
250
|
+
const { featurePoints } = this.fullDetector!.detect(inputData);
|
|
247
251
|
const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(
|
|
248
252
|
featurePoints,
|
|
249
253
|
targetIndexes,
|
|
@@ -252,22 +256,73 @@ class Controller {
|
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
async _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number) {
|
|
255
|
-
const { worldCoords, screenCoords } = this.tracker!.track(
|
|
259
|
+
const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0 } = this.tracker!.track(
|
|
256
260
|
inputData,
|
|
257
261
|
lastModelViewTransform,
|
|
258
262
|
targetIndex,
|
|
259
263
|
);
|
|
260
|
-
|
|
264
|
+
|
|
265
|
+
const state = this.trackingStates[targetIndex];
|
|
266
|
+
if (!state.pointStabilities) state.pointStabilities = [];
|
|
267
|
+
if (!state.pointStabilities[octaveIndex]) {
|
|
268
|
+
// Initialize stabilities for this octave if not exists
|
|
269
|
+
const numPoints = (this.tracker as any).prebuiltData[targetIndex][octaveIndex].px.length;
|
|
270
|
+
state.pointStabilities[octaveIndex] = new Float32Array(numPoints).fill(0.5); // Start at 0.5
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const stabilities = state.pointStabilities[octaveIndex];
|
|
274
|
+
const currentStabilities: number[] = [];
|
|
275
|
+
|
|
276
|
+
// Update all points in this octave
|
|
277
|
+
for (let i = 0; i < stabilities.length; i++) {
|
|
278
|
+
const isTracked = indices.includes(i);
|
|
279
|
+
if (isTracked) {
|
|
280
|
+
stabilities[i] = Math.min(1.0, stabilities[i] + 0.35); // Fast recovery (approx 3 frames)
|
|
281
|
+
} else {
|
|
282
|
+
stabilities[i] = Math.max(0.0, stabilities[i] - 0.12); // Slightly more forgiving loss
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Collect stabilities and FILTER OUT excessive flickerers (Dead Zone)
|
|
287
|
+
const filteredWorldCoords = [];
|
|
288
|
+
const filteredScreenCoords = [];
|
|
289
|
+
const filteredStabilities = [];
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < indices.length; i++) {
|
|
292
|
+
const s = stabilities[indices[i]];
|
|
293
|
+
if (s > 0.3) { // Hard Cutoff: points with <30% stability are ignored
|
|
294
|
+
filteredWorldCoords.push(worldCoords[i]);
|
|
295
|
+
filteredScreenCoords.push(screenCoords[i]);
|
|
296
|
+
filteredStabilities.push(s);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// STRICT QUALITY CHECK: Prevent "sticky" tracking on background noise.
|
|
301
|
+
// We require a minimum number of high-confidence AND STABLE points.
|
|
302
|
+
const stableAndReliable = reliabilities.filter((r: number, idx: number) => r > 0.75 && stabilities[indices[idx]] > 0.5).length;
|
|
303
|
+
|
|
304
|
+
if (stableAndReliable < 6 || filteredWorldCoords.length < 8) {
|
|
305
|
+
return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
|
|
306
|
+
}
|
|
307
|
+
|
|
261
308
|
const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
|
|
262
|
-
worldCoords,
|
|
263
|
-
screenCoords,
|
|
309
|
+
worldCoords: filteredWorldCoords,
|
|
310
|
+
screenCoords: filteredScreenCoords,
|
|
311
|
+
stabilities: filteredStabilities
|
|
264
312
|
});
|
|
265
|
-
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
modelViewTransform,
|
|
316
|
+
screenCoords: filteredScreenCoords,
|
|
317
|
+
reliabilities: reliabilities.filter((_, idx) => stabilities[indices[idx]] > 0.3),
|
|
318
|
+
stabilities: filteredStabilities
|
|
319
|
+
};
|
|
266
320
|
}
|
|
267
321
|
|
|
268
322
|
processVideo(input: any) {
|
|
269
323
|
if (this.processingVideo) return;
|
|
270
324
|
this.processingVideo = true;
|
|
325
|
+
const currentLoopId = ++loopIdCounter; // Added for ghost loop prevention
|
|
271
326
|
|
|
272
327
|
this.trackingStates = [];
|
|
273
328
|
for (let i = 0; i < (this.markerDimensions?.length || 0); i++) {
|
|
@@ -282,7 +337,7 @@ class Controller {
|
|
|
282
337
|
|
|
283
338
|
const startProcessing = async () => {
|
|
284
339
|
while (true) {
|
|
285
|
-
if (!this.processingVideo) break;
|
|
340
|
+
if (!this.processingVideo || currentLoopId !== loopIdCounter) break;
|
|
286
341
|
|
|
287
342
|
const inputData = this.inputLoader.loadInput(input);
|
|
288
343
|
const nTracking = this.trackingStates.reduce((acc, s) => acc + (!!s.isTracking ? 1 : 0), 0);
|
|
@@ -309,15 +364,21 @@ class Controller {
|
|
|
309
364
|
const trackingState = this.trackingStates[i];
|
|
310
365
|
|
|
311
366
|
if (trackingState.isTracking) {
|
|
312
|
-
|
|
367
|
+
const result = await this._trackAndUpdate(
|
|
313
368
|
inputData,
|
|
314
369
|
trackingState.currentModelViewTransform,
|
|
315
370
|
i,
|
|
316
371
|
);
|
|
317
|
-
if (modelViewTransform === null) {
|
|
372
|
+
if (result === null || result.modelViewTransform === null) {
|
|
318
373
|
trackingState.isTracking = false;
|
|
374
|
+
trackingState.screenCoords = [];
|
|
375
|
+
trackingState.reliabilities = [];
|
|
376
|
+
trackingState.stabilities = [];
|
|
319
377
|
} else {
|
|
320
|
-
trackingState.currentModelViewTransform = modelViewTransform;
|
|
378
|
+
trackingState.currentModelViewTransform = result.modelViewTransform;
|
|
379
|
+
trackingState.screenCoords = result.screenCoords;
|
|
380
|
+
trackingState.reliabilities = result.reliabilities;
|
|
381
|
+
trackingState.stabilities = result.stabilities;
|
|
321
382
|
}
|
|
322
383
|
}
|
|
323
384
|
|
|
@@ -332,7 +393,14 @@ class Controller {
|
|
|
332
393
|
|
|
333
394
|
if (trackingState.showing) {
|
|
334
395
|
const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
|
|
335
|
-
|
|
396
|
+
|
|
397
|
+
// Calculate confidence score based on point stability
|
|
398
|
+
const stabilities = trackingState.stabilities || [];
|
|
399
|
+
const avgStability = stabilities.length > 0
|
|
400
|
+
? stabilities.reduce((a: number, b: number) => a + b, 0) / stabilities.length
|
|
401
|
+
: 0;
|
|
402
|
+
|
|
403
|
+
const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix, { stability: avgStability });
|
|
336
404
|
trackingState.trackingMatrix = filteredMatrix;
|
|
337
405
|
|
|
338
406
|
let finalMatrix = [...filteredMatrix];
|
|
@@ -349,7 +417,10 @@ class Controller {
|
|
|
349
417
|
type: "updateMatrix",
|
|
350
418
|
targetIndex: i,
|
|
351
419
|
worldMatrix: finalMatrix,
|
|
352
|
-
modelViewTransform: trackingState.currentModelViewTransform
|
|
420
|
+
modelViewTransform: trackingState.currentModelViewTransform,
|
|
421
|
+
screenCoords: trackingState.screenCoords,
|
|
422
|
+
reliabilities: trackingState.reliabilities,
|
|
423
|
+
stabilities: trackingState.stabilities
|
|
353
424
|
});
|
|
354
425
|
}
|
|
355
426
|
}
|
|
@@ -372,9 +443,8 @@ class Controller {
|
|
|
372
443
|
|
|
373
444
|
async detect(input: any) {
|
|
374
445
|
const inputData = this.inputLoader.loadInput(input);
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
return { featurePoints, debugExtra };
|
|
446
|
+
const { featurePoints } = this.fullDetector!.detect(inputData);
|
|
447
|
+
return { featurePoints, debugExtra: {} };
|
|
378
448
|
}
|
|
379
449
|
|
|
380
450
|
async match(featurePoints: any, targetIndex: number) {
|
|
@@ -397,11 +467,18 @@ class Controller {
|
|
|
397
467
|
_workerMatch(featurePoints: any, targetIndexes: number[]): Promise<any> {
|
|
398
468
|
return new Promise((resolve) => {
|
|
399
469
|
if (!this.worker) {
|
|
400
|
-
this._matchOnMainThread(featurePoints, targetIndexes).then(resolve);
|
|
470
|
+
this._matchOnMainThread(featurePoints, targetIndexes).then(resolve).catch(() => resolve({ targetIndex: -1 }));
|
|
401
471
|
return;
|
|
402
472
|
}
|
|
403
473
|
|
|
474
|
+
const timeout = setTimeout(() => {
|
|
475
|
+
this.workerMatchDone = null;
|
|
476
|
+
resolve({ targetIndex: -1 });
|
|
477
|
+
}, WORKER_TIMEOUT_MS);
|
|
478
|
+
|
|
404
479
|
this.workerMatchDone = (data: any) => {
|
|
480
|
+
clearTimeout(timeout);
|
|
481
|
+
this.workerMatchDone = null;
|
|
405
482
|
resolve({
|
|
406
483
|
targetIndex: data.targetIndex,
|
|
407
484
|
modelViewTransform: data.modelViewTransform,
|
|
@@ -460,19 +537,27 @@ class Controller {
|
|
|
460
537
|
_workerTrackUpdate(modelViewTransform: number[][], trackingFeatures: any): Promise<any> {
|
|
461
538
|
return new Promise((resolve) => {
|
|
462
539
|
if (!this.worker) {
|
|
463
|
-
this._trackUpdateOnMainThread(modelViewTransform, trackingFeatures).then(resolve);
|
|
540
|
+
this._trackUpdateOnMainThread(modelViewTransform, trackingFeatures).then(resolve).catch(() => resolve(null));
|
|
464
541
|
return;
|
|
465
542
|
}
|
|
466
543
|
|
|
544
|
+
const timeout = setTimeout(() => {
|
|
545
|
+
this.workerTrackDone = null;
|
|
546
|
+
resolve(null);
|
|
547
|
+
}, WORKER_TIMEOUT_MS);
|
|
548
|
+
|
|
467
549
|
this.workerTrackDone = (data: any) => {
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
this.workerTrackDone = null;
|
|
468
552
|
resolve(data.modelViewTransform);
|
|
469
553
|
};
|
|
470
|
-
const { worldCoords, screenCoords } = trackingFeatures;
|
|
554
|
+
const { worldCoords, screenCoords, stabilities } = trackingFeatures;
|
|
471
555
|
this.worker.postMessage({
|
|
472
556
|
type: "trackUpdate",
|
|
473
557
|
modelViewTransform,
|
|
474
558
|
worldCoords,
|
|
475
559
|
screenCoords,
|
|
560
|
+
stabilities
|
|
476
561
|
});
|
|
477
562
|
});
|
|
478
563
|
}
|
|
@@ -483,11 +568,12 @@ class Controller {
|
|
|
483
568
|
this.mainThreadEstimator = new Estimator(this.projectionTransform);
|
|
484
569
|
}
|
|
485
570
|
|
|
486
|
-
const { worldCoords, screenCoords } = trackingFeatures;
|
|
571
|
+
const { worldCoords, screenCoords, stabilities } = trackingFeatures;
|
|
487
572
|
return this.mainThreadEstimator.refineEstimate({
|
|
488
573
|
initialModelViewTransform: modelViewTransform,
|
|
489
574
|
worldCoords,
|
|
490
575
|
screenCoords,
|
|
576
|
+
stabilities
|
|
491
577
|
});
|
|
492
578
|
}
|
|
493
579
|
|
|
@@ -53,11 +53,12 @@ onmessage = (msg) => {
|
|
|
53
53
|
break;
|
|
54
54
|
|
|
55
55
|
case "trackUpdate":
|
|
56
|
-
const { modelViewTransform, worldCoords, screenCoords } = data;
|
|
56
|
+
const { modelViewTransform, worldCoords, screenCoords, stabilities } = data;
|
|
57
57
|
const finalModelViewTransform = estimator.refineEstimate({
|
|
58
58
|
initialModelViewTransform: modelViewTransform,
|
|
59
59
|
worldCoords,
|
|
60
60
|
screenCoords,
|
|
61
|
+
stabilities, // Stability-based weights
|
|
61
62
|
});
|
|
62
63
|
postMessage({
|
|
63
64
|
type: "trackUpdateDone",
|
|
@@ -21,6 +21,7 @@ const refineEstimate = ({
|
|
|
21
21
|
projectionTransform,
|
|
22
22
|
worldCoords,
|
|
23
23
|
screenCoords,
|
|
24
|
+
stabilities, // Stability-based weighting
|
|
24
25
|
}) => {
|
|
25
26
|
// Question: shall we normlize the screen coords as well?
|
|
26
27
|
// Question: do we need to normlize the scale as well, i.e. make coords from -1 to 1
|
|
@@ -74,6 +75,7 @@ const refineEstimate = ({
|
|
|
74
75
|
projectionTransform,
|
|
75
76
|
worldCoords: normalizedWorldCoords,
|
|
76
77
|
screenCoords,
|
|
78
|
+
stabilities, // Pass weights to ICP
|
|
77
79
|
inlierProb: inlierProbs[i],
|
|
78
80
|
});
|
|
79
81
|
|
|
@@ -113,6 +115,7 @@ const _doICP = ({
|
|
|
113
115
|
projectionTransform,
|
|
114
116
|
worldCoords,
|
|
115
117
|
screenCoords,
|
|
118
|
+
stabilities,
|
|
116
119
|
inlierProb,
|
|
117
120
|
}) => {
|
|
118
121
|
const isRobustMode = inlierProb < 1;
|
|
@@ -196,7 +199,13 @@ const _doICP = ({
|
|
|
196
199
|
});
|
|
197
200
|
|
|
198
201
|
if (isRobustMode) {
|
|
199
|
-
const
|
|
202
|
+
const robustW = (1.0 - E[n] / K2) * (1.0 - E[n] / K2);
|
|
203
|
+
|
|
204
|
+
// Log-weighted stability: suppresses vibrators aggressively but allows recovery
|
|
205
|
+
const s = stabilities ? stabilities[n] : 1.0;
|
|
206
|
+
const stabilityW = s * Math.log10(9 * s + 1);
|
|
207
|
+
|
|
208
|
+
const W = robustW * stabilityW;
|
|
200
209
|
|
|
201
210
|
for (let j = 0; j < 2; j++) {
|
|
202
211
|
for (let i = 0; i < 6; i++) {
|
|
@@ -206,8 +215,16 @@ const _doICP = ({
|
|
|
206
215
|
dU.push([dxs[n] * W]);
|
|
207
216
|
dU.push([dys[n] * W]);
|
|
208
217
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
const s = stabilities ? stabilities[n] : 1.0;
|
|
219
|
+
const W = s * Math.log10(9 * s + 1);
|
|
220
|
+
|
|
221
|
+
for (let j = 0; j < 2; j++) {
|
|
222
|
+
for (let i = 0; i < 6; i++) {
|
|
223
|
+
J_U_S[j][i] *= W;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
dU.push([dxs[n] * W]);
|
|
227
|
+
dU.push([dys[n] * W]);
|
|
211
228
|
}
|
|
212
229
|
|
|
213
230
|
for (let i = 0; i < J_U_S.length; i++) {
|
|
@@ -39,7 +39,7 @@ export interface ControllerFeature extends Feature {
|
|
|
39
39
|
/**
|
|
40
40
|
* Hook to filter or modify the final world matrix
|
|
41
41
|
*/
|
|
42
|
-
filterWorldMatrix?(targetIndex: number, worldMatrix: number[]): number[];
|
|
42
|
+
filterWorldMatrix?(targetIndex: number, worldMatrix: number[], context?: any): number[];
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Hook to decide if a target should be shown
|
|
@@ -27,11 +27,11 @@ export class FeatureManager {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[]): number[] {
|
|
30
|
+
applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[], context?: any): number[] {
|
|
31
31
|
let result = worldMatrix;
|
|
32
32
|
for (const feature of this.features) {
|
|
33
33
|
if (feature.enabled && feature.filterWorldMatrix) {
|
|
34
|
-
result = feature.filterWorldMatrix(targetIndex, result);
|
|
34
|
+
result = feature.filterWorldMatrix(targetIndex, result, context);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
return result;
|
|
@@ -30,9 +30,19 @@ export class OneEuroFilterFeature implements ControllerFeature {
|
|
|
30
30
|
return this.filters[targetIndex];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
|
|
33
|
+
filterWorldMatrix(targetIndex: number, worldMatrix: number[], context?: any): number[] {
|
|
34
34
|
if (!this.enabled) return worldMatrix;
|
|
35
|
+
|
|
35
36
|
const filter = this.getFilter(targetIndex);
|
|
37
|
+
const stability = context?.stability ?? 1.0;
|
|
38
|
+
|
|
39
|
+
// Dynamic Cutoff: If points are very stable (1.0), use higher cutoff (less responsiveness loss).
|
|
40
|
+
// If points are unstable (0.3), use much lower cutoff (heavy smoothing).
|
|
41
|
+
// We use a squared curve for even more aggressive suppression of jitter on unstable points.
|
|
42
|
+
const dynamicMinCutOff = this.minCutOff * (0.05 + Math.pow(stability, 2) * 0.95);
|
|
43
|
+
filter.minCutOff = dynamicMinCutOff;
|
|
44
|
+
filter.beta = this.beta;
|
|
45
|
+
|
|
36
46
|
return filter.filter(Date.now(), worldMatrix);
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -235,7 +235,7 @@ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop
|
|
|
235
235
|
if (dist !== minD) {
|
|
236
236
|
queue.push({ node: childrenOrIndices[i], d: dist });
|
|
237
237
|
} else {
|
|
238
|
-
_query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop });
|
|
238
|
+
_query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1 });
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
@@ -11,7 +11,6 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
|
|
|
11
11
|
import { DetectorLite } from "./detector/detector-lite.js";
|
|
12
12
|
import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
|
|
13
13
|
import * as msgpack from "@msgpack/msgpack";
|
|
14
|
-
import { WorkerPool } from "./utils/worker-pool.js";
|
|
15
14
|
|
|
16
15
|
// Detect environment
|
|
17
16
|
const isNode = typeof process !== "undefined" &&
|
|
@@ -22,40 +21,9 @@ const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
|
|
|
22
21
|
|
|
23
22
|
export class OfflineCompiler {
|
|
24
23
|
data: any = null;
|
|
25
|
-
workerPool: WorkerPool | null = null;
|
|
26
24
|
|
|
27
25
|
constructor() {
|
|
28
|
-
|
|
29
|
-
if (!isNode) {
|
|
30
|
-
console.log("🌐 OfflineCompiler: Browser mode (no workers)");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async _initNodeWorkers() {
|
|
35
|
-
try {
|
|
36
|
-
const pathModule = "path";
|
|
37
|
-
const urlModule = "url";
|
|
38
|
-
const osModule = "os";
|
|
39
|
-
const workerThreadsModule = "node:worker_threads";
|
|
40
|
-
|
|
41
|
-
const [path, url, os, { Worker }] = await Promise.all([
|
|
42
|
-
import(/* @vite-ignore */ pathModule),
|
|
43
|
-
import(/* @vite-ignore */ urlModule),
|
|
44
|
-
import(/* @vite-ignore */ osModule),
|
|
45
|
-
import(/* @vite-ignore */ workerThreadsModule)
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
const __filename = url.fileURLToPath(import.meta.url);
|
|
49
|
-
const __dirname = path.dirname(__filename);
|
|
50
|
-
const workerPath = path.join(__dirname, "node-worker.js");
|
|
51
|
-
|
|
52
|
-
// Limit workers to avoid freezing system
|
|
53
|
-
const numWorkers = Math.min(os.cpus().length, 4);
|
|
54
|
-
|
|
55
|
-
this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
|
|
58
|
-
}
|
|
26
|
+
console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
|
|
59
27
|
}
|
|
60
28
|
|
|
61
29
|
async compileImageTargets(images: any[], progressCallback: (p: number) => void) {
|
|
@@ -108,25 +76,7 @@ export class OfflineCompiler {
|
|
|
108
76
|
}
|
|
109
77
|
|
|
110
78
|
async _compileTarget(targetImages: any[], progressCallback: (p: number) => void) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (this.workerPool) {
|
|
114
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
115
|
-
const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
|
|
116
|
-
return this.workerPool!.runTask({
|
|
117
|
-
type: 'compile-all', // 🚀 MOONSHOT: Combined task
|
|
118
|
-
targetImage,
|
|
119
|
-
onProgress: (p: number) => {
|
|
120
|
-
progressMap[index] = p;
|
|
121
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
122
|
-
progressCallback(sum / targetImages.length);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
return Promise.all(wrappedPromises);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Fallback or non-worker implementation: run match and track sequentially
|
|
79
|
+
// Run match and track sequentially to match browser behavior exactly
|
|
130
80
|
const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
|
|
131
81
|
const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
|
|
132
82
|
|
|
@@ -140,27 +90,6 @@ export class OfflineCompiler {
|
|
|
140
90
|
const percentPerImage = 100 / targetImages.length;
|
|
141
91
|
let currentPercent = 0;
|
|
142
92
|
|
|
143
|
-
if (isNode) await this._initNodeWorkers();
|
|
144
|
-
if (this.workerPool) {
|
|
145
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
146
|
-
|
|
147
|
-
const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
|
|
148
|
-
return this.workerPool!.runTask({
|
|
149
|
-
type: 'match',
|
|
150
|
-
targetImage,
|
|
151
|
-
percentPerImage,
|
|
152
|
-
basePercent: 0,
|
|
153
|
-
onProgress: (p: number) => {
|
|
154
|
-
progressMap[index] = p;
|
|
155
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
156
|
-
progressCallback(sum);
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return Promise.all(wrappedPromises);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
93
|
const results = [];
|
|
165
94
|
for (let i = 0; i < targetImages.length; i++) {
|
|
166
95
|
const targetImage = targetImages[i];
|
|
@@ -201,24 +130,6 @@ export class OfflineCompiler {
|
|
|
201
130
|
const percentPerImage = 100 / targetImages.length;
|
|
202
131
|
let currentPercent = 0;
|
|
203
132
|
|
|
204
|
-
if (this.workerPool) {
|
|
205
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
206
|
-
const wrappedPromises = targetImages.map((targetImage: any, index: number) => {
|
|
207
|
-
return this.workerPool!.runTask({
|
|
208
|
-
type: 'compile',
|
|
209
|
-
targetImage,
|
|
210
|
-
percentPerImage,
|
|
211
|
-
basePercent: 0,
|
|
212
|
-
onProgress: (p: number) => {
|
|
213
|
-
progressMap[index] = p;
|
|
214
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
215
|
-
progressCallback(sum);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
return Promise.all(wrappedPromises);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
133
|
const results = [];
|
|
223
134
|
for (let i = 0; i < targetImages.length; i++) {
|
|
224
135
|
const targetImage = targetImages[i];
|
|
@@ -480,9 +391,7 @@ export class OfflineCompiler {
|
|
|
480
391
|
}
|
|
481
392
|
|
|
482
393
|
async destroy() {
|
|
483
|
-
|
|
484
|
-
await this.workerPool.destroy();
|
|
485
|
-
}
|
|
394
|
+
// No workers to destroy
|
|
486
395
|
}
|
|
487
396
|
|
|
488
397
|
|
|
@@ -13,7 +13,13 @@ export interface SimpleAROptions {
|
|
|
13
13
|
scale?: number;
|
|
14
14
|
onFound?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
15
15
|
onLost?: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
16
|
-
onUpdate?: ((data: {
|
|
16
|
+
onUpdate?: ((data: {
|
|
17
|
+
targetIndex: number,
|
|
18
|
+
worldMatrix: number[],
|
|
19
|
+
screenCoords?: { x: number, y: number }[],
|
|
20
|
+
reliabilities?: number[],
|
|
21
|
+
stabilities?: number[]
|
|
22
|
+
}) => void) | null;
|
|
17
23
|
cameraConfig?: MediaStreamConstraints['video'];
|
|
18
24
|
debug?: boolean;
|
|
19
25
|
}
|
|
@@ -25,7 +31,13 @@ class SimpleAR {
|
|
|
25
31
|
scaleMultiplier: number;
|
|
26
32
|
onFound: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
27
33
|
onLost: ((data: { targetIndex: number }) => void | Promise<void>) | null;
|
|
28
|
-
onUpdateCallback: ((data: {
|
|
34
|
+
onUpdateCallback: ((data: {
|
|
35
|
+
targetIndex: number,
|
|
36
|
+
worldMatrix: number[],
|
|
37
|
+
screenCoords?: { x: number, y: number }[],
|
|
38
|
+
reliabilities?: number[],
|
|
39
|
+
stabilities?: number[]
|
|
40
|
+
}) => void) | null;
|
|
29
41
|
cameraConfig: MediaStreamConstraints['video'];
|
|
30
42
|
debug: boolean;
|
|
31
43
|
|
|
@@ -151,7 +163,7 @@ class SimpleAR {
|
|
|
151
163
|
if (this.debug) this._updateDebugPanel(this.isTracking);
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
const { targetIndex, worldMatrix, modelViewTransform } = data;
|
|
166
|
+
const { targetIndex, worldMatrix, modelViewTransform, screenCoords, reliabilities, stabilities } = data;
|
|
155
167
|
|
|
156
168
|
if (worldMatrix) {
|
|
157
169
|
if (!this.isTracking) {
|
|
@@ -162,31 +174,61 @@ class SimpleAR {
|
|
|
162
174
|
|
|
163
175
|
this.lastMatrix = worldMatrix;
|
|
164
176
|
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
// We use the matrix from the controller directly (it's already filtered there)
|
|
178
|
+
this._positionOverlay(modelViewTransform, targetIndex);
|
|
179
|
+
|
|
180
|
+
// Project points to screen coordinates
|
|
181
|
+
let projectedPoints = [];
|
|
182
|
+
if (screenCoords && screenCoords.length > 0) {
|
|
183
|
+
const containerRect = this.container.getBoundingClientRect();
|
|
184
|
+
const videoW = this.video!.videoWidth;
|
|
185
|
+
const videoH = this.video!.videoHeight;
|
|
186
|
+
const isPortrait = containerRect.height > containerRect.width;
|
|
187
|
+
const isVideoLandscape = videoW > videoH;
|
|
188
|
+
const needsRotation = isPortrait && isVideoLandscape;
|
|
189
|
+
const proj = this.controller!.projectionTransform;
|
|
190
|
+
|
|
191
|
+
const vW = needsRotation ? videoH : videoW;
|
|
192
|
+
const vH = needsRotation ? videoW : videoH;
|
|
193
|
+
const pScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
194
|
+
const dW = vW * pScale;
|
|
195
|
+
const dH = vH * pScale;
|
|
196
|
+
const oX = (containerRect.width - dW) / 2;
|
|
197
|
+
const oY = (containerRect.height - dH) / 2;
|
|
198
|
+
|
|
199
|
+
projectedPoints = screenCoords.map((p: any) => {
|
|
200
|
+
let sx, sy;
|
|
201
|
+
if (needsRotation) {
|
|
202
|
+
sx = oX + (dW / 2) - (p.y - proj[1][2]) * pScale;
|
|
203
|
+
sy = oY + (dH / 2) + (p.x - proj[0][2]) * pScale;
|
|
204
|
+
} else {
|
|
205
|
+
sx = oX + (dW / 2) + (p.x - proj[0][2]) * pScale;
|
|
206
|
+
sy = oY + (dH / 2) + (p.y - proj[1][2]) * pScale;
|
|
207
|
+
}
|
|
208
|
+
return { x: sx, y: sy };
|
|
209
|
+
});
|
|
167
210
|
}
|
|
168
211
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
[smoothedFlat[0], smoothedFlat[1], smoothedFlat[2], smoothedFlat[3]],
|
|
177
|
-
[smoothedFlat[4], smoothedFlat[5], smoothedFlat[6], smoothedFlat[7]],
|
|
178
|
-
[smoothedFlat[8], smoothedFlat[9], smoothedFlat[10], smoothedFlat[11]]
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
this._positionOverlay(smoothedMVT, targetIndex);
|
|
182
|
-
this.onUpdateCallback && this.onUpdateCallback({ targetIndex, worldMatrix });
|
|
212
|
+
this.onUpdateCallback && this.onUpdateCallback({
|
|
213
|
+
targetIndex,
|
|
214
|
+
worldMatrix,
|
|
215
|
+
screenCoords: projectedPoints,
|
|
216
|
+
reliabilities,
|
|
217
|
+
stabilities
|
|
218
|
+
});
|
|
183
219
|
|
|
184
220
|
} else {
|
|
185
221
|
if (this.isTracking) {
|
|
186
222
|
this.isTracking = false;
|
|
187
|
-
if (this.filters[targetIndex]) this.filters[targetIndex].reset();
|
|
188
223
|
this.overlay && (this.overlay.style.opacity = '0');
|
|
189
224
|
this.onLost && this.onLost({ targetIndex });
|
|
225
|
+
this.onUpdateCallback && this.onUpdateCallback({
|
|
226
|
+
targetIndex,
|
|
227
|
+
worldMatrix: null as any,
|
|
228
|
+
screenCoords: [],
|
|
229
|
+
reliabilities: [],
|
|
230
|
+
stabilities: []
|
|
231
|
+
});
|
|
190
232
|
}
|
|
191
233
|
}
|
|
192
234
|
}
|