@srsergio/taptapp-ar 1.0.79 → 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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/compiler/controller.d.ts +22 -6
  4. package/dist/compiler/controller.js +99 -26
  5. package/dist/compiler/controller.worker.js +2 -1
  6. package/dist/compiler/estimation/refine-estimate.d.ts +2 -1
  7. package/dist/compiler/estimation/refine-estimate.js +18 -5
  8. package/dist/compiler/features/feature-base.d.ts +1 -1
  9. package/dist/compiler/features/feature-manager.d.ts +1 -1
  10. package/dist/compiler/features/feature-manager.js +2 -2
  11. package/dist/compiler/features/one-euro-filter-feature.d.ts +1 -1
  12. package/dist/compiler/features/one-euro-filter-feature.js +8 -1
  13. package/dist/compiler/matching/matching.js +1 -1
  14. package/dist/compiler/simple-ar.d.ts +12 -0
  15. package/dist/compiler/simple-ar.js +46 -19
  16. package/dist/compiler/tracker/tracker.d.ts +10 -0
  17. package/dist/compiler/tracker/tracker.js +6 -4
  18. package/dist/react/TaptappAR.js +26 -2
  19. package/dist/react/use-ar.d.ts +7 -0
  20. package/dist/react/use-ar.js +15 -1
  21. package/package.json +24 -2
  22. package/src/compiler/controller.ts +112 -26
  23. package/src/compiler/controller.worker.js +2 -1
  24. package/src/compiler/estimation/refine-estimate.js +20 -3
  25. package/src/compiler/features/feature-base.ts +1 -1
  26. package/src/compiler/features/feature-manager.ts +2 -2
  27. package/src/compiler/features/one-euro-filter-feature.ts +11 -1
  28. package/src/compiler/matching/matching.js +1 -1
  29. package/src/compiler/simple-ar.ts +62 -20
  30. package/src/compiler/tracker/tracker.js +7 -4
  31. package/src/react/TaptappAR.tsx +38 -1
  32. package/src/react/use-ar.ts +23 -1
@@ -11,6 +11,13 @@ export class Tracker {
11
11
  templateBuffer: Float32Array<ArrayBuffer>;
12
12
  dummyRun(inputData: any): void;
13
13
  track(inputData: any, lastModelViewTransform: any, targetIndex: any): {
14
+ worldCoords: never[];
15
+ screenCoords: never[];
16
+ reliabilities: never[];
17
+ debugExtra: {};
18
+ indices?: undefined;
19
+ octaveIndex?: undefined;
20
+ } | {
14
21
  worldCoords: {
15
22
  x: number;
16
23
  y: number;
@@ -20,6 +27,9 @@ export class Tracker {
20
27
  x: number;
21
28
  y: number;
22
29
  }[];
30
+ reliabilities: number[];
31
+ indices: number[];
32
+ octaveIndex: any;
23
33
  debugExtra: {};
24
34
  };
25
35
  lastOctaveIndex: any[] | undefined;
@@ -1,9 +1,9 @@
1
1
  import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
2
2
  const AR2_DEFAULT_TS = 6;
3
3
  const AR2_DEFAULT_TS_GAP = 1;
4
- const AR2_SEARCH_SIZE = 34; // Increased from 18 to 34 for much better fast-motion tracking
4
+ const AR2_SEARCH_SIZE = 25; // Reduced from 34 to 25 to prevent background latching
5
5
  const AR2_SEARCH_GAP = 1;
6
- const AR2_SIM_THRESH = 0.6;
6
+ const AR2_SIM_THRESH = 0.65; // Increased from 0.6 to reduce false positives
7
7
  const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
8
8
  class Tracker {
9
9
  constructor(markerDimensions, trackingDataList, projectionTransform, inputWidth, inputHeight, debugMode = false) {
@@ -82,6 +82,7 @@ class Tracker {
82
82
  const screenCoords = [];
83
83
  const goodTrack = [];
84
84
  const { px, py, s: scale } = trackingFrame;
85
+ const reliabilities = [];
85
86
  for (let i = 0; i < matchingPoints.length; i++) {
86
87
  if (sim[i] > AR2_SIM_THRESH && i < px.length) {
87
88
  goodTrack.push(i);
@@ -92,6 +93,7 @@ class Tracker {
92
93
  y: py[i] / scale,
93
94
  z: 0,
94
95
  });
96
+ reliabilities.push(sim[i]);
95
97
  }
96
98
  }
97
99
  // 2.1 Spatial distribution check: Avoid getting stuck in corners/noise
@@ -110,7 +112,7 @@ class Tracker {
110
112
  const detectedDiagonal = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2);
111
113
  // If the points cover too little space compared to the screen size of the marker, it's a glitch
112
114
  if (detectedDiagonal < screenW * 0.15) {
113
- return { worldCoords: [], screenCoords: [], debugExtra };
115
+ return { worldCoords: [], screenCoords: [], reliabilities: [], debugExtra };
114
116
  }
115
117
  }
116
118
  if (this.debugMode) {
@@ -122,7 +124,7 @@ class Tracker {
122
124
  trackedPoints: screenCoords,
123
125
  };
124
126
  }
125
- return { worldCoords, screenCoords, debugExtra };
127
+ return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, debugExtra };
126
128
  }
127
129
  /**
128
130
  * Pure JS implementation of NCC matching
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { useAR } from "./use-ar.js";
4
4
  export const TaptappAR = ({ config, className = "", showScanningOverlay = true, showErrorOverlay = true }) => {
5
- const { containerRef, overlayRef, status, toggleVideo } = useAR(config);
5
+ const { containerRef, overlayRef, status, toggleVideo, trackedPoints } = useAR(config);
6
6
  // Simple heuristic to determine if it's a video or image
7
7
  // based on the presence of videoSrc and common extensions
8
8
  const isVideo = useMemo(() => {
@@ -12,7 +12,13 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
12
12
  const url = config.videoSrc.toLowerCase().split('?')[0];
13
13
  return videoExtensions.some(ext => url.endsWith(ext)) || config.videoSrc.includes('video');
14
14
  }, [config.videoSrc]);
15
- return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), _jsx("style", { children: `
15
+ return (_jsxs("div", { className: `taptapp-ar-wrapper ${className} ${status}`, style: { position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }, children: [showScanningOverlay && status === "scanning" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-scanning", children: _jsxs("div", { className: "scanning-content", children: [_jsxs("div", { className: "scanning-frame", children: [_jsx("img", { className: "target-preview", src: config.targetImageSrc, alt: "Target", crossOrigin: "anonymous" }), _jsx("div", { className: "scanning-line" })] }), _jsx("p", { className: "scanning-text", children: "Apunta a la imagen para comenzar" })] }) })), showErrorOverlay && status === "error" && (_jsx("div", { className: "taptapp-ar-overlay taptapp-ar-error", children: _jsxs("div", { className: "error-content", children: [_jsx("span", { className: "error-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "error-title", children: "No se pudo iniciar AR" }), _jsx("p", { className: "error-text", children: "Verifica los permisos de c\u00E1mara" }), _jsx("button", { className: "retry-btn", onClick: () => window.location.reload(), children: "Reintentar" })] }) })), _jsx("div", { ref: containerRef, className: "taptapp-ar-container", onClick: toggleVideo, style: { width: '100%', height: '100%' }, children: isVideo ? (_jsx("video", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc, preload: "auto", loop: true, playsInline: true, muted: true, crossOrigin: "anonymous" })) : (_jsx("img", { ref: overlayRef, className: "taptapp-ar-overlay-element", src: config.videoSrc || config.targetImageSrc, crossOrigin: "anonymous", alt: "AR Overlay" })) }), status === "tracking" && (_jsx("div", { className: "taptapp-ar-points-overlay", children: trackedPoints.filter(p => p.reliability > 0.7).map((point, i) => (_jsx("div", { className: "tracking-point", style: {
16
+ left: `${point.x}px`,
17
+ top: `${point.y}px`,
18
+ width: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
19
+ height: `${(2 + point.reliability * 6) * (0.4 + point.stability * 0.6)}px`,
20
+ opacity: (0.3 + (point.reliability * 0.4)) * (0.2 + point.stability * 0.8)
21
+ } }, i))) })), _jsx("style", { children: `
16
22
  .taptapp-ar-wrapper {
17
23
  background: #000;
18
24
  color: white;
@@ -96,5 +102,23 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
96
102
  pointer-events: none;
97
103
  transition: opacity 0.3s ease;
98
104
  }
105
+ .taptapp-ar-points-overlay {
106
+ position: absolute;
107
+ top: 0;
108
+ left: 0;
109
+ width: 100%;
110
+ height: 100%;
111
+ pointer-events: none;
112
+ z-index: 100; /* High z-index to be above overlay */
113
+ }
114
+ .tracking-point {
115
+ position: absolute;
116
+ background: black;
117
+ border: 1px solid rgba(255,255,255,0.5); /* Better contrast */
118
+ border-radius: 50%;
119
+ transform: translate(-50%, -50%);
120
+ box-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
121
+ pointer-events: none;
122
+ }
99
123
  ` })] }));
100
124
  };
@@ -1,10 +1,17 @@
1
1
  import type { ARConfig } from "./types.js";
2
2
  export type ARStatus = "scanning" | "tracking" | "error";
3
+ export interface TrackedPoint {
4
+ x: number;
5
+ y: number;
6
+ reliability: number;
7
+ stability: number;
8
+ }
3
9
  export interface UseARReturn {
4
10
  containerRef: React.RefObject<HTMLDivElement>;
5
11
  overlayRef: React.RefObject<HTMLVideoElement | HTMLImageElement>;
6
12
  status: ARStatus;
7
13
  isPlaying: boolean;
8
14
  toggleVideo: () => Promise<void>;
15
+ trackedPoints: TrackedPoint[];
9
16
  }
10
17
  export declare const useAR: (config: ARConfig) => UseARReturn;
@@ -4,6 +4,7 @@ export const useAR = (config) => {
4
4
  const overlayRef = useRef(null);
5
5
  const [status, setStatus] = useState("scanning");
6
6
  const [isPlaying, setIsPlaying] = useState(false);
7
+ const [trackedPoints, setTrackedPoints] = useState([]);
7
8
  const arInstanceRef = useRef(null);
8
9
  const toggleVideo = useCallback(async () => {
9
10
  const overlay = overlayRef.current;
@@ -39,6 +40,17 @@ export const useAR = (config) => {
39
40
  overlay: overlayRef.current,
40
41
  scale: config.scale,
41
42
  debug: false,
43
+ onUpdate: ({ screenCoords, reliabilities, stabilities }) => {
44
+ if (screenCoords && reliabilities && stabilities) {
45
+ const points = screenCoords.map((p, i) => ({
46
+ x: p.x,
47
+ y: p.y,
48
+ reliability: reliabilities[i],
49
+ stability: stabilities[i]
50
+ }));
51
+ setTrackedPoints(points);
52
+ }
53
+ },
42
54
  onFound: async ({ targetIndex }) => {
43
55
  console.log(`🎯 Target ${targetIndex} detected!`);
44
56
  if (!isMounted)
@@ -60,6 +72,7 @@ export const useAR = (config) => {
60
72
  if (!isMounted)
61
73
  return;
62
74
  setStatus("scanning");
75
+ setTrackedPoints([]);
63
76
  const overlay = overlayRef.current;
64
77
  if (overlay instanceof HTMLVideoElement) {
65
78
  overlay.pause();
@@ -90,6 +103,7 @@ export const useAR = (config) => {
90
103
  overlayRef,
91
104
  status,
92
105
  isPlaying,
93
- toggleVideo
106
+ toggleVideo,
107
+ trackedPoints
94
108
  };
95
109
  };
package/package.json CHANGED
@@ -1,7 +1,29 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.79",
4
- "description": "AR Compiler for Node.js and Browser",
3
+ "version": "1.0.80",
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
+ "keywords": [
6
+ "augmented reality",
7
+ "ar",
8
+ "image tracking",
9
+ "computer vision",
10
+ "webar",
11
+ "a-frame",
12
+ "three.js",
13
+ "ar.js",
14
+ "mindar",
15
+ "tracking",
16
+ "opencv",
17
+ "javascript",
18
+ "webgl",
19
+ "cross-platform",
20
+ "no-wasm",
21
+ "no-tfjs",
22
+ "webxr",
23
+ "natural-feature-tracking"
24
+ ],
25
+ "author": "Sergio Lázaro <slazaro.dev@gmail.com>",
26
+ "license": "MIT",
5
27
  "repository": {
6
28
  "type": "git",
7
29
  "url": "git+https://github.com/srsergiolazaro/taptapp-ar.git"
@@ -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 { CropDetectionFeature } from "./features/crop-detection-feature.js";
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 = 5; // More grace when partially hidden
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
- this.featureManager.addFeature(new CropDetectionFeature());
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
- const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
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 cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
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
- if (worldCoords.length < 8) return null; // Resynced with Matcher (8 points) to allow initial detection
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
- return modelViewTransform;
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
- let modelViewTransform = await this._trackAndUpdate(
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
- const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix);
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 cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
376
- const { featurePoints, debugExtra } = cropFeature!.detect(inputData, false);
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 W = (1.0 - E[n] / K2) * (1.0 - E[n] / K2);
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
- dU.push([dxs[n]]);
210
- dU.push([dys[n]]);
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