@srsergio/taptapp-ar 1.0.77 → 1.0.79
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/dist/compiler/controller.d.ts +2 -6
- package/dist/compiler/controller.js +43 -44
- package/dist/compiler/features/auto-rotation-feature.d.ts +15 -0
- package/dist/compiler/features/auto-rotation-feature.js +30 -0
- package/dist/compiler/features/crop-detection-feature.d.ts +18 -0
- package/dist/compiler/features/crop-detection-feature.js +26 -0
- package/dist/compiler/features/feature-base.d.ts +46 -0
- package/dist/compiler/features/feature-base.js +1 -0
- package/dist/compiler/features/feature-manager.d.ts +12 -0
- package/dist/compiler/features/feature-manager.js +55 -0
- package/dist/compiler/features/one-euro-filter-feature.d.ts +15 -0
- package/dist/compiler/features/one-euro-filter-feature.js +37 -0
- package/dist/compiler/features/temporal-filter-feature.d.ts +19 -0
- package/dist/compiler/features/temporal-filter-feature.js +57 -0
- package/dist/compiler/offline-compiler.d.ts +92 -8
- package/dist/compiler/offline-compiler.js +3 -86
- package/dist/react/TaptappAR.js +3 -0
- package/dist/react/use-ar.js +1 -0
- package/package.json +1 -1
- package/src/compiler/FEATURES.md +40 -0
- package/src/compiler/controller.ts +54 -42
- package/src/compiler/features/auto-rotation-feature.ts +36 -0
- package/src/compiler/features/crop-detection-feature.ts +31 -0
- package/src/compiler/features/feature-base.ts +48 -0
- package/src/compiler/features/feature-manager.ts +65 -0
- package/src/compiler/features/one-euro-filter-feature.ts +44 -0
- package/src/compiler/features/temporal-filter-feature.ts +68 -0
- package/src/compiler/offline-compiler.ts +3 -94
- package/src/react/TaptappAR.tsx +3 -0
- package/src/react/use-ar.ts +1 -0
|
@@ -10,7 +10,6 @@ import { extractTrackingFeatures } from "./tracker/extract-utils.js";
|
|
|
10
10
|
import { DetectorLite } from "./detector/detector-lite.js";
|
|
11
11
|
import { build as hierarchicalClusteringBuild } from "./matching/hierarchical-clustering.js";
|
|
12
12
|
import * as msgpack from "@msgpack/msgpack";
|
|
13
|
-
import { WorkerPool } from "./utils/worker-pool.js";
|
|
14
13
|
// Detect environment
|
|
15
14
|
const isNode = typeof process !== "undefined" &&
|
|
16
15
|
process.versions != null &&
|
|
@@ -18,35 +17,8 @@ const isNode = typeof process !== "undefined" &&
|
|
|
18
17
|
const CURRENT_VERSION = 7; // Protocol v7: Moonshot - 4-bit Packed Tracking Data
|
|
19
18
|
export class OfflineCompiler {
|
|
20
19
|
data = null;
|
|
21
|
-
workerPool = null;
|
|
22
20
|
constructor() {
|
|
23
|
-
|
|
24
|
-
if (!isNode) {
|
|
25
|
-
console.log("🌐 OfflineCompiler: Browser mode (no workers)");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async _initNodeWorkers() {
|
|
29
|
-
try {
|
|
30
|
-
const pathModule = "path";
|
|
31
|
-
const urlModule = "url";
|
|
32
|
-
const osModule = "os";
|
|
33
|
-
const workerThreadsModule = "node:worker_threads";
|
|
34
|
-
const [path, url, os, { Worker }] = await Promise.all([
|
|
35
|
-
import(/* @vite-ignore */ pathModule),
|
|
36
|
-
import(/* @vite-ignore */ urlModule),
|
|
37
|
-
import(/* @vite-ignore */ osModule),
|
|
38
|
-
import(/* @vite-ignore */ workerThreadsModule)
|
|
39
|
-
]);
|
|
40
|
-
const __filename = url.fileURLToPath(import.meta.url);
|
|
41
|
-
const __dirname = path.dirname(__filename);
|
|
42
|
-
const workerPath = path.join(__dirname, "node-worker.js");
|
|
43
|
-
// Limit workers to avoid freezing system
|
|
44
|
-
const numWorkers = Math.min(os.cpus().length, 4);
|
|
45
|
-
this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
48
|
-
console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
|
|
49
|
-
}
|
|
21
|
+
console.log("⚡ OfflineCompiler: Main thread mode (no workers)");
|
|
50
22
|
}
|
|
51
23
|
async compileImageTargets(images, progressCallback) {
|
|
52
24
|
console.time("⏱️ Compilación total");
|
|
@@ -86,24 +58,7 @@ export class OfflineCompiler {
|
|
|
86
58
|
return this.data;
|
|
87
59
|
}
|
|
88
60
|
async _compileTarget(targetImages, progressCallback) {
|
|
89
|
-
|
|
90
|
-
await this._initNodeWorkers();
|
|
91
|
-
if (this.workerPool) {
|
|
92
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
93
|
-
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
94
|
-
return this.workerPool.runTask({
|
|
95
|
-
type: 'compile-all', // 🚀 MOONSHOT: Combined task
|
|
96
|
-
targetImage,
|
|
97
|
-
onProgress: (p) => {
|
|
98
|
-
progressMap[index] = p;
|
|
99
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
100
|
-
progressCallback(sum / targetImages.length);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
return Promise.all(wrappedPromises);
|
|
105
|
-
}
|
|
106
|
-
// Fallback or non-worker implementation: run match and track sequentially
|
|
61
|
+
// Run match and track sequentially to match browser behavior exactly
|
|
107
62
|
const matchingResults = await this._compileMatch(targetImages, (p) => progressCallback(p * 0.5));
|
|
108
63
|
const trackingResults = await this._compileTrack(targetImages, (p) => progressCallback(50 + p * 0.5));
|
|
109
64
|
return targetImages.map((_, i) => ({
|
|
@@ -114,25 +69,6 @@ export class OfflineCompiler {
|
|
|
114
69
|
async _compileMatch(targetImages, progressCallback) {
|
|
115
70
|
const percentPerImage = 100 / targetImages.length;
|
|
116
71
|
let currentPercent = 0;
|
|
117
|
-
if (isNode)
|
|
118
|
-
await this._initNodeWorkers();
|
|
119
|
-
if (this.workerPool) {
|
|
120
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
121
|
-
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
122
|
-
return this.workerPool.runTask({
|
|
123
|
-
type: 'match',
|
|
124
|
-
targetImage,
|
|
125
|
-
percentPerImage,
|
|
126
|
-
basePercent: 0,
|
|
127
|
-
onProgress: (p) => {
|
|
128
|
-
progressMap[index] = p;
|
|
129
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
130
|
-
progressCallback(sum);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
return Promise.all(wrappedPromises);
|
|
135
|
-
}
|
|
136
72
|
const results = [];
|
|
137
73
|
for (let i = 0; i < targetImages.length; i++) {
|
|
138
74
|
const targetImage = targetImages[i];
|
|
@@ -165,23 +101,6 @@ export class OfflineCompiler {
|
|
|
165
101
|
async _compileTrack(targetImages, progressCallback) {
|
|
166
102
|
const percentPerImage = 100 / targetImages.length;
|
|
167
103
|
let currentPercent = 0;
|
|
168
|
-
if (this.workerPool) {
|
|
169
|
-
const progressMap = new Float32Array(targetImages.length);
|
|
170
|
-
const wrappedPromises = targetImages.map((targetImage, index) => {
|
|
171
|
-
return this.workerPool.runTask({
|
|
172
|
-
type: 'compile',
|
|
173
|
-
targetImage,
|
|
174
|
-
percentPerImage,
|
|
175
|
-
basePercent: 0,
|
|
176
|
-
onProgress: (p) => {
|
|
177
|
-
progressMap[index] = p;
|
|
178
|
-
const sum = progressMap.reduce((a, b) => a + b, 0);
|
|
179
|
-
progressCallback(sum);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
return Promise.all(wrappedPromises);
|
|
184
|
-
}
|
|
185
104
|
const results = [];
|
|
186
105
|
for (let i = 0; i < targetImages.length; i++) {
|
|
187
106
|
const targetImage = targetImages[i];
|
|
@@ -407,9 +326,7 @@ export class OfflineCompiler {
|
|
|
407
326
|
};
|
|
408
327
|
}
|
|
409
328
|
async destroy() {
|
|
410
|
-
|
|
411
|
-
await this.workerPool.destroy();
|
|
412
|
-
}
|
|
329
|
+
// No workers to destroy
|
|
413
330
|
}
|
|
414
331
|
_pack4Bit(data) {
|
|
415
332
|
const length = data.length;
|
package/dist/react/TaptappAR.js
CHANGED
package/dist/react/use-ar.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Taptapp AR Controller Features
|
|
2
|
+
|
|
3
|
+
Este documento detalla las características modulares y mejoras de rendimiento implementadas en el controlador de Taptapp AR. Cada una de estas funcionalidades puede ser activada o desactivada rápidamente desde el código para pruebas de rendimiento o debugging.
|
|
4
|
+
|
|
5
|
+
## 1. One Euro Filter (Smoothing)
|
|
6
|
+
- **Trata de:** Un filtro adaptativo de primer orden para suavizar señales ruidosas.
|
|
7
|
+
- **Qué aporta:** Reduce drásticamente el "jitter" (temblor) visual del objeto AR cuando el marcador está estático o se mueve lentamente. Proporciona una experiencia mucho más estable y premium.
|
|
8
|
+
- **Requisito / Dependencia:** Ninguno. Es independiente.
|
|
9
|
+
- **Archivo:** `features/one-euro-filter-feature.ts`
|
|
10
|
+
|
|
11
|
+
## 2. Temporal Filter (Warmup & Miss Tolerance)
|
|
12
|
+
- **Trata de:** Lógica de persistencia temporal que gestiona estados de "calentamiento" y "pérdida".
|
|
13
|
+
- **Qué aporta:**
|
|
14
|
+
- **Warmup:** Evita falsos positivos al requerir que el marcador sea detectado consistentemente durante N cuadros antes de mostrarlo.
|
|
15
|
+
- **Miss Tolerance:** Mantiene el objeto visible durante breves oclusiones o fallos rápidos del tracker, evitando que el objeto parpadee o desaparezca instantáneamente.
|
|
16
|
+
- **Requisito / Dependencia:** Ninguno.
|
|
17
|
+
- **Archivo:** `features/temporal-filter-feature.ts`
|
|
18
|
+
|
|
19
|
+
## 3. Crop Detection (Movement Optimization)
|
|
20
|
+
- **Trata de:** Optimización que analiza solo regiones con movimiento en lugar de procesar toda la imagen.
|
|
21
|
+
- **Qué aporta:** Mejora significativa en el rendimiento (FPS) y reduce el uso de CPU. Al detectar puntos de interés solo en áreas activas, se ignora el fondo estático.
|
|
22
|
+
- **Requisito / Dependencia:** Depende de `CropDetector`.
|
|
23
|
+
- **Archivo:** `features/crop-detection-feature.ts`
|
|
24
|
+
|
|
25
|
+
## 4. Auto Rotation Matrix
|
|
26
|
+
- **Trata de:** Ajuste automático de la matriz de transformación basada en la orientación del video de entrada.
|
|
27
|
+
- **Qué aporta:** Permite que el AR funcione correctamente tanto en modo horizontal como vertical (Portrait) sin que el desarrollador tenga que rotar manualmente las coordenadas.
|
|
28
|
+
- **Requisito / Dependencia:** Ninguno.
|
|
29
|
+
- **Archivo:** `features/auto-rotation-feature.ts`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Cómo desactivar una característica rápidamente
|
|
34
|
+
|
|
35
|
+
En `packages/taptapp-ar/src/compiler/controller.ts`, puedes comentar la línea donde se añade la característica en el constructor o configurar el flag `enabled: false` en la instancia.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// Ejemplo para desactivar suavizado
|
|
39
|
+
this.featureManager.getFeature("one-euro-filter").enabled = false;
|
|
40
|
+
```
|
|
@@ -2,7 +2,11 @@ import { Tracker } from "./tracker/tracker.js";
|
|
|
2
2
|
import { CropDetector } from "./detector/crop-detector.js";
|
|
3
3
|
import { OfflineCompiler as Compiler } from "./offline-compiler.js";
|
|
4
4
|
import { InputLoader } from "./input-loader.js";
|
|
5
|
-
import {
|
|
5
|
+
import { FeatureManager } from "./features/feature-manager.js";
|
|
6
|
+
import { OneEuroFilterFeature } from "./features/one-euro-filter-feature.js";
|
|
7
|
+
import { TemporalFilterFeature } from "./features/temporal-filter-feature.js";
|
|
8
|
+
import { AutoRotationFeature } from "./features/auto-rotation-feature.js";
|
|
9
|
+
import { CropDetectionFeature } from "./features/crop-detection-feature.js";
|
|
6
10
|
|
|
7
11
|
let ControllerWorker: any;
|
|
8
12
|
|
|
@@ -41,11 +45,6 @@ class Controller {
|
|
|
41
45
|
inputWidth: number;
|
|
42
46
|
inputHeight: number;
|
|
43
47
|
maxTrack: number;
|
|
44
|
-
filterMinCF: number;
|
|
45
|
-
filterBeta: number;
|
|
46
|
-
warmupTolerance: number;
|
|
47
|
-
missTolerance: number;
|
|
48
|
-
cropDetector: CropDetector;
|
|
49
48
|
inputLoader: InputLoader;
|
|
50
49
|
markerDimensions: any[] | null = null;
|
|
51
50
|
onUpdate: ((data: any) => void) | null;
|
|
@@ -62,6 +61,7 @@ class Controller {
|
|
|
62
61
|
workerTrackDone: ((data: any) => void) | null = null;
|
|
63
62
|
mainThreadMatcher: any;
|
|
64
63
|
mainThreadEstimator: any;
|
|
64
|
+
featureManager: FeatureManager;
|
|
65
65
|
|
|
66
66
|
constructor({
|
|
67
67
|
inputWidth,
|
|
@@ -78,17 +78,32 @@ class Controller {
|
|
|
78
78
|
this.inputWidth = inputWidth;
|
|
79
79
|
this.inputHeight = inputHeight;
|
|
80
80
|
this.maxTrack = maxTrack;
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
|
|
82
|
+
this.featureManager = new FeatureManager();
|
|
83
|
+
this.featureManager.addFeature(new OneEuroFilterFeature(
|
|
84
|
+
filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF,
|
|
85
|
+
filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta
|
|
86
|
+
));
|
|
87
|
+
this.featureManager.addFeature(new TemporalFilterFeature(
|
|
88
|
+
warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance,
|
|
89
|
+
missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance
|
|
90
|
+
));
|
|
91
|
+
this.featureManager.addFeature(new AutoRotationFeature());
|
|
92
|
+
this.featureManager.addFeature(new CropDetectionFeature());
|
|
93
|
+
|
|
86
94
|
this.inputLoader = new InputLoader(this.inputWidth, this.inputHeight);
|
|
87
95
|
this.onUpdate = onUpdate;
|
|
88
96
|
this.debugMode = debugMode;
|
|
89
97
|
this.worker = worker;
|
|
90
98
|
if (this.worker) this._setupWorkerListener();
|
|
91
99
|
|
|
100
|
+
this.featureManager.init({
|
|
101
|
+
inputWidth: this.inputWidth,
|
|
102
|
+
inputHeight: this.inputHeight,
|
|
103
|
+
projectionTransform: [], // Will be set below
|
|
104
|
+
debugMode: this.debugMode
|
|
105
|
+
});
|
|
106
|
+
|
|
92
107
|
const near = 10;
|
|
93
108
|
const far = 100000;
|
|
94
109
|
const fovy = (45.0 * Math.PI) / 180;
|
|
@@ -100,6 +115,13 @@ class Controller {
|
|
|
100
115
|
[0, 0, 1],
|
|
101
116
|
];
|
|
102
117
|
|
|
118
|
+
this.featureManager.init({
|
|
119
|
+
inputWidth: this.inputWidth,
|
|
120
|
+
inputHeight: this.inputHeight,
|
|
121
|
+
projectionTransform: this.projectionTransform,
|
|
122
|
+
debugMode: this.debugMode
|
|
123
|
+
});
|
|
124
|
+
|
|
103
125
|
this.projectionMatrix = this._glProjectionMatrix({
|
|
104
126
|
projectionTransform: this.projectionTransform,
|
|
105
127
|
width: this.inputWidth,
|
|
@@ -197,7 +219,8 @@ class Controller {
|
|
|
197
219
|
|
|
198
220
|
dummyRun(input: any) {
|
|
199
221
|
const inputData = this.inputLoader.loadInput(input);
|
|
200
|
-
this.
|
|
222
|
+
const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
|
|
223
|
+
cropFeature?.detect(inputData, false);
|
|
201
224
|
this.tracker!.dummyRun(inputData);
|
|
202
225
|
}
|
|
203
226
|
|
|
@@ -219,7 +242,8 @@ class Controller {
|
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
async _detectAndMatch(inputData: any, targetIndexes: number[]) {
|
|
222
|
-
const
|
|
245
|
+
const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
|
|
246
|
+
const { featurePoints } = cropFeature!.detect(inputData, true);
|
|
223
247
|
const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(
|
|
224
248
|
featurePoints,
|
|
225
249
|
targetIndexes,
|
|
@@ -253,7 +277,6 @@ class Controller {
|
|
|
253
277
|
currentModelViewTransform: null,
|
|
254
278
|
trackCount: 0,
|
|
255
279
|
trackMiss: 0,
|
|
256
|
-
filter: new OneEuroFilter({ minCutOff: this.filterMinCF, beta: this.filterBeta }),
|
|
257
280
|
});
|
|
258
281
|
}
|
|
259
282
|
|
|
@@ -298,46 +321,34 @@ class Controller {
|
|
|
298
321
|
}
|
|
299
322
|
}
|
|
300
323
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
trackingState.trackMiss = 0;
|
|
304
|
-
trackingState.trackCount += 1;
|
|
305
|
-
if (trackingState.trackCount > this.warmupTolerance) {
|
|
306
|
-
trackingState.showing = true;
|
|
307
|
-
trackingState.trackingMatrix = null;
|
|
308
|
-
trackingState.filter.reset();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
324
|
+
const wasShowing = trackingState.showing;
|
|
325
|
+
trackingState.showing = this.featureManager.shouldShow(i, trackingState.isTracking);
|
|
312
326
|
|
|
313
|
-
if (trackingState.showing) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (trackingState.trackMiss > this.missTolerance) {
|
|
318
|
-
trackingState.showing = false;
|
|
319
|
-
trackingState.trackingMatrix = null;
|
|
320
|
-
this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
trackingState.trackMiss = 0;
|
|
324
|
-
}
|
|
327
|
+
if (wasShowing && !trackingState.showing) {
|
|
328
|
+
trackingState.trackingMatrix = null;
|
|
329
|
+
this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
|
|
330
|
+
this.featureManager.notifyUpdate({ type: "reset", targetIndex: i });
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
if (trackingState.showing) {
|
|
328
334
|
const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix);
|
|
336
|
+
trackingState.trackingMatrix = filteredMatrix;
|
|
337
|
+
|
|
338
|
+
let finalMatrix = [...filteredMatrix];
|
|
331
339
|
|
|
332
340
|
const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
|
|
333
341
|
if (isInputRotated) {
|
|
334
|
-
|
|
342
|
+
const rotationFeature = this.featureManager.getFeature<AutoRotationFeature>("auto-rotation");
|
|
343
|
+
if (rotationFeature) {
|
|
344
|
+
finalMatrix = rotationFeature.rotate(finalMatrix);
|
|
345
|
+
}
|
|
335
346
|
}
|
|
336
347
|
|
|
337
348
|
this.onUpdate && this.onUpdate({
|
|
338
349
|
type: "updateMatrix",
|
|
339
350
|
targetIndex: i,
|
|
340
|
-
worldMatrix:
|
|
351
|
+
worldMatrix: finalMatrix,
|
|
341
352
|
modelViewTransform: trackingState.currentModelViewTransform
|
|
342
353
|
});
|
|
343
354
|
}
|
|
@@ -361,7 +372,8 @@ class Controller {
|
|
|
361
372
|
|
|
362
373
|
async detect(input: any) {
|
|
363
374
|
const inputData = this.inputLoader.loadInput(input);
|
|
364
|
-
const
|
|
375
|
+
const cropFeature = this.featureManager.getFeature<CropDetectionFeature>("crop-detection");
|
|
376
|
+
const { featurePoints, debugExtra } = cropFeature!.detect(inputData, false);
|
|
365
377
|
return { featurePoints, debugExtra };
|
|
366
378
|
}
|
|
367
379
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ControllerFeature } from "./feature-base.js";
|
|
2
|
+
|
|
3
|
+
export class AutoRotationFeature implements ControllerFeature {
|
|
4
|
+
id = "auto-rotation";
|
|
5
|
+
name = "Auto Rotation Matrix";
|
|
6
|
+
description = "Automatically adjusts the world matrix if the input video is rotated (e.g. portrait mode).";
|
|
7
|
+
enabled = true;
|
|
8
|
+
|
|
9
|
+
private inputWidth: number = 0;
|
|
10
|
+
private inputHeight: number = 0;
|
|
11
|
+
|
|
12
|
+
init(context: { inputWidth: number; inputHeight: number }) {
|
|
13
|
+
this.inputWidth = context.inputWidth;
|
|
14
|
+
this.inputHeight = context.inputHeight;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
|
|
18
|
+
if (!this.enabled) return worldMatrix;
|
|
19
|
+
|
|
20
|
+
// Check if input is rotated (this logic might need the actual current input dimensions)
|
|
21
|
+
// For now, we'll assume the controller passes the 'isRotated' info or we detect it
|
|
22
|
+
// But since this is a matrix post-process, we can just apply it if needed.
|
|
23
|
+
return worldMatrix;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// We might need a way to pass the 'currentInput' to the feature.
|
|
27
|
+
// Actually, the controller can just call this if it detects rotation.
|
|
28
|
+
rotate(m: number[]): number[] {
|
|
29
|
+
return [
|
|
30
|
+
-m[1], m[0], m[2], m[3],
|
|
31
|
+
-m[5], m[4], m[6], m[7],
|
|
32
|
+
-m[9], m[8], m[10], m[11],
|
|
33
|
+
-m[13], m[12], m[14], m[15],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ControllerFeature, FeatureContext } from "./feature-base.js";
|
|
2
|
+
import { CropDetector } from "../detector/crop-detector.js";
|
|
3
|
+
|
|
4
|
+
export class CropDetectionFeature implements ControllerFeature {
|
|
5
|
+
id = "crop-detection";
|
|
6
|
+
name = "Crop Detection";
|
|
7
|
+
description = "Optimizes detection by focusing on areas with motion, reducing CPU usage.";
|
|
8
|
+
enabled = true;
|
|
9
|
+
|
|
10
|
+
private cropDetector: CropDetector | null = null;
|
|
11
|
+
private debugMode: boolean = false;
|
|
12
|
+
|
|
13
|
+
init(context: FeatureContext) {
|
|
14
|
+
this.debugMode = context.debugMode;
|
|
15
|
+
this.cropDetector = new CropDetector(context.inputWidth, context.inputHeight, this.debugMode);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
detect(inputData: any, isMoving: boolean = true) {
|
|
19
|
+
if (!this.enabled || !this.cropDetector) {
|
|
20
|
+
// Fallback to full detection if disabled?
|
|
21
|
+
// Actually CropDetector.detect is just full detection.
|
|
22
|
+
// We'll expose the methods here.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isMoving && this.enabled) {
|
|
26
|
+
return this.cropDetector!.detectMoving(inputData);
|
|
27
|
+
} else {
|
|
28
|
+
return this.cropDetector!.detect(inputData);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface FeatureContext {
|
|
2
|
+
inputWidth: number;
|
|
3
|
+
inputHeight: number;
|
|
4
|
+
projectionTransform: number[][];
|
|
5
|
+
debugMode: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Feature {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
|
|
14
|
+
// Lifecycle hooks
|
|
15
|
+
init?(context: FeatureContext): void;
|
|
16
|
+
onUpdate?(data: any): void;
|
|
17
|
+
dispose?(): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Controller feature interface for processing hooks
|
|
22
|
+
*/
|
|
23
|
+
export interface ControllerFeature extends Feature {
|
|
24
|
+
/**
|
|
25
|
+
* Called before processing a frame
|
|
26
|
+
*/
|
|
27
|
+
beforeProcess?(inputData: any): void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Called after detection/matching
|
|
31
|
+
*/
|
|
32
|
+
afterMatch?(result: { targetIndex: number, modelViewTransform: number[][] | null }): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Called after tracking update
|
|
36
|
+
*/
|
|
37
|
+
afterTrack?(result: { targetIndex: number, modelViewTransform: number[][] | null }): void;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook to filter or modify the final world matrix
|
|
41
|
+
*/
|
|
42
|
+
filterWorldMatrix?(targetIndex: number, worldMatrix: number[]): number[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook to decide if a target should be shown
|
|
46
|
+
*/
|
|
47
|
+
shouldShow?(targetIndex: number, isTracking: boolean): boolean;
|
|
48
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ControllerFeature, FeatureContext } from "./feature-base.js";
|
|
2
|
+
|
|
3
|
+
export class FeatureManager {
|
|
4
|
+
private features: ControllerFeature[] = [];
|
|
5
|
+
|
|
6
|
+
addFeature(feature: ControllerFeature) {
|
|
7
|
+
this.features.push(feature);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getFeature<T extends ControllerFeature>(id: string): T | undefined {
|
|
11
|
+
return this.features.find(f => f.id === id) as T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
init(context: FeatureContext) {
|
|
15
|
+
for (const feature of this.features) {
|
|
16
|
+
if (feature.enabled && feature.init) {
|
|
17
|
+
feature.init(context);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
beforeProcess(inputData: any) {
|
|
23
|
+
for (const feature of this.features) {
|
|
24
|
+
if (feature.enabled && feature.beforeProcess) {
|
|
25
|
+
feature.beforeProcess(inputData);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[]): number[] {
|
|
31
|
+
let result = worldMatrix;
|
|
32
|
+
for (const feature of this.features) {
|
|
33
|
+
if (feature.enabled && feature.filterWorldMatrix) {
|
|
34
|
+
result = feature.filterWorldMatrix(targetIndex, result);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
shouldShow(targetIndex: number, isTracking: boolean): boolean {
|
|
41
|
+
let show = isTracking;
|
|
42
|
+
for (const feature of this.features) {
|
|
43
|
+
if (feature.enabled && feature.shouldShow) {
|
|
44
|
+
show = feature.shouldShow(targetIndex, isTracking);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return show;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
notifyUpdate(data: any) {
|
|
51
|
+
for (const feature of this.features) {
|
|
52
|
+
if (feature.enabled && feature.onUpdate) {
|
|
53
|
+
feature.onUpdate(data);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dispose() {
|
|
59
|
+
for (const feature of this.features) {
|
|
60
|
+
if (feature.dispose) {
|
|
61
|
+
feature.dispose();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ControllerFeature, FeatureContext } from "./feature-base.js";
|
|
2
|
+
import { OneEuroFilter } from "../../libs/one-euro-filter.js";
|
|
3
|
+
|
|
4
|
+
export class OneEuroFilterFeature implements ControllerFeature {
|
|
5
|
+
id = "one-euro-filter";
|
|
6
|
+
name = "One Euro Filter";
|
|
7
|
+
description = "Smooths the tracking matrix to reduce jitter using a One Euro Filter.";
|
|
8
|
+
enabled = true;
|
|
9
|
+
|
|
10
|
+
private filters: OneEuroFilter[] = [];
|
|
11
|
+
private minCutOff: number;
|
|
12
|
+
private beta: number;
|
|
13
|
+
|
|
14
|
+
constructor(minCutOff: number = 0.5, beta: number = 0.1) {
|
|
15
|
+
this.minCutOff = minCutOff;
|
|
16
|
+
this.beta = beta;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
init(context: FeatureContext) {
|
|
20
|
+
// We'll initialize filters lazily or based on target count if known
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private getFilter(targetIndex: number): OneEuroFilter {
|
|
24
|
+
if (!this.filters[targetIndex]) {
|
|
25
|
+
this.filters[targetIndex] = new OneEuroFilter({
|
|
26
|
+
minCutOff: this.minCutOff,
|
|
27
|
+
beta: this.beta
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return this.filters[targetIndex];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[] {
|
|
34
|
+
if (!this.enabled) return worldMatrix;
|
|
35
|
+
const filter = this.getFilter(targetIndex);
|
|
36
|
+
return filter.filter(Date.now(), worldMatrix);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onUpdate(data: any) {
|
|
40
|
+
if (data.type === "reset" && data.targetIndex !== undefined) {
|
|
41
|
+
this.filters[data.targetIndex]?.reset();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|