@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.
- 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/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 +26 -2
- package/dist/react/use-ar.d.ts +7 -0
- package/dist/react/use-ar.js +15 -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/simple-ar.ts +62 -20
- package/src/compiler/tracker/tracker.js +7 -4
- package/src/react/TaptappAR.tsx +38 -1
- 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 =
|
|
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
|
package/dist/react/TaptappAR.js
CHANGED
|
@@ -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("
|
|
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
|
};
|
package/dist/react/use-ar.d.ts
CHANGED
|
@@ -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;
|
package/dist/react/use-ar.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "AR
|
|
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 {
|
|
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
|
|