@srsergio/taptapp-ar 1.0.76 → 1.0.78

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 (28) hide show
  1. package/dist/compiler/controller.d.ts +2 -6
  2. package/dist/compiler/controller.js +45 -46
  3. package/dist/compiler/detector/crop-detector.d.ts +1 -0
  4. package/dist/compiler/detector/crop-detector.js +11 -14
  5. package/dist/compiler/features/auto-rotation-feature.d.ts +15 -0
  6. package/dist/compiler/features/auto-rotation-feature.js +30 -0
  7. package/dist/compiler/features/crop-detection-feature.d.ts +18 -0
  8. package/dist/compiler/features/crop-detection-feature.js +26 -0
  9. package/dist/compiler/features/feature-base.d.ts +46 -0
  10. package/dist/compiler/features/feature-base.js +1 -0
  11. package/dist/compiler/features/feature-manager.d.ts +12 -0
  12. package/dist/compiler/features/feature-manager.js +55 -0
  13. package/dist/compiler/features/one-euro-filter-feature.d.ts +15 -0
  14. package/dist/compiler/features/one-euro-filter-feature.js +37 -0
  15. package/dist/compiler/features/temporal-filter-feature.d.ts +19 -0
  16. package/dist/compiler/features/temporal-filter-feature.js +57 -0
  17. package/dist/compiler/tracker/tracker.js +1 -1
  18. package/package.json +1 -1
  19. package/src/compiler/FEATURES.md +40 -0
  20. package/src/compiler/controller.ts +55 -43
  21. package/src/compiler/detector/crop-detector.js +11 -14
  22. package/src/compiler/features/auto-rotation-feature.ts +36 -0
  23. package/src/compiler/features/crop-detection-feature.ts +31 -0
  24. package/src/compiler/features/feature-base.ts +48 -0
  25. package/src/compiler/features/feature-manager.ts +65 -0
  26. package/src/compiler/features/one-euro-filter-feature.ts +44 -0
  27. package/src/compiler/features/temporal-filter-feature.ts +68 -0
  28. package/src/compiler/tracker/tracker.js +1 -1
@@ -1,6 +1,6 @@
1
1
  import { Tracker } from "./tracker/tracker.js";
2
- import { CropDetector } from "./detector/crop-detector.js";
3
2
  import { InputLoader } from "./input-loader.js";
3
+ import { FeatureManager } from "./features/feature-manager.js";
4
4
  export interface ControllerOptions {
5
5
  inputWidth: number;
6
6
  inputHeight: number;
@@ -17,11 +17,6 @@ declare class Controller {
17
17
  inputWidth: number;
18
18
  inputHeight: number;
19
19
  maxTrack: number;
20
- filterMinCF: number;
21
- filterBeta: number;
22
- warmupTolerance: number;
23
- missTolerance: number;
24
- cropDetector: CropDetector;
25
20
  inputLoader: InputLoader;
26
21
  markerDimensions: any[] | null;
27
22
  onUpdate: ((data: any) => void) | null;
@@ -38,6 +33,7 @@ declare class Controller {
38
33
  workerTrackDone: ((data: any) => void) | null;
39
34
  mainThreadMatcher: any;
40
35
  mainThreadEstimator: any;
36
+ featureManager: FeatureManager;
41
37
  constructor({ inputWidth, inputHeight, onUpdate, debugMode, maxTrack, warmupTolerance, missTolerance, filterMinCF, filterBeta, worker, }: ControllerOptions);
42
38
  _setupWorkerListener(): void;
43
39
  _ensureWorker(): void;
@@ -1,8 +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
- import { OneEuroFilter } from "../libs/one-euro-filter.js";
4
+ import { FeatureManager } from "./features/feature-manager.js";
5
+ import { OneEuroFilterFeature } from "./features/one-euro-filter-feature.js";
6
+ import { TemporalFilterFeature } from "./features/temporal-filter-feature.js";
7
+ import { AutoRotationFeature } from "./features/auto-rotation-feature.js";
8
+ import { CropDetectionFeature } from "./features/crop-detection-feature.js";
6
9
  let ControllerWorker;
7
10
  // Conditional import for worker to avoid crash in non-vite environments
8
11
  const getControllerWorker = async () => {
@@ -26,11 +29,6 @@ class Controller {
26
29
  inputWidth;
27
30
  inputHeight;
28
31
  maxTrack;
29
- filterMinCF;
30
- filterBeta;
31
- warmupTolerance;
32
- missTolerance;
33
- cropDetector;
34
32
  inputLoader;
35
33
  markerDimensions = null;
36
34
  onUpdate;
@@ -47,21 +45,28 @@ class Controller {
47
45
  workerTrackDone = null;
48
46
  mainThreadMatcher;
49
47
  mainThreadEstimator;
48
+ featureManager;
50
49
  constructor({ inputWidth, inputHeight, onUpdate = null, debugMode = false, maxTrack = 1, warmupTolerance = null, missTolerance = null, filterMinCF = null, filterBeta = null, worker = null, }) {
51
50
  this.inputWidth = inputWidth;
52
51
  this.inputHeight = inputHeight;
53
52
  this.maxTrack = maxTrack;
54
- this.filterMinCF = filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF;
55
- this.filterBeta = filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta;
56
- this.warmupTolerance = warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance;
57
- this.missTolerance = missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance;
58
- this.cropDetector = new CropDetector(this.inputWidth, this.inputHeight, debugMode);
53
+ this.featureManager = new FeatureManager();
54
+ this.featureManager.addFeature(new OneEuroFilterFeature(filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF, filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta));
55
+ this.featureManager.addFeature(new TemporalFilterFeature(warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance, missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance));
56
+ this.featureManager.addFeature(new AutoRotationFeature());
57
+ this.featureManager.addFeature(new CropDetectionFeature());
59
58
  this.inputLoader = new InputLoader(this.inputWidth, this.inputHeight);
60
59
  this.onUpdate = onUpdate;
61
60
  this.debugMode = debugMode;
62
61
  this.worker = worker;
63
62
  if (this.worker)
64
63
  this._setupWorkerListener();
64
+ this.featureManager.init({
65
+ inputWidth: this.inputWidth,
66
+ inputHeight: this.inputHeight,
67
+ projectionTransform: [], // Will be set below
68
+ debugMode: this.debugMode
69
+ });
65
70
  const near = 10;
66
71
  const far = 100000;
67
72
  const fovy = (45.0 * Math.PI) / 180;
@@ -71,6 +76,12 @@ class Controller {
71
76
  [0, f, this.inputHeight / 2],
72
77
  [0, 0, 1],
73
78
  ];
79
+ this.featureManager.init({
80
+ inputWidth: this.inputWidth,
81
+ inputHeight: this.inputHeight,
82
+ projectionTransform: this.projectionTransform,
83
+ debugMode: this.debugMode
84
+ });
74
85
  this.projectionMatrix = this._glProjectionMatrix({
75
86
  projectionTransform: this.projectionTransform,
76
87
  width: this.inputWidth,
@@ -149,7 +160,8 @@ class Controller {
149
160
  }
150
161
  dummyRun(input) {
151
162
  const inputData = this.inputLoader.loadInput(input);
152
- this.cropDetector.detect(inputData);
163
+ const cropFeature = this.featureManager.getFeature("crop-detection");
164
+ cropFeature?.detect(inputData, false);
153
165
  this.tracker.dummyRun(inputData);
154
166
  }
155
167
  getProjectionMatrix() {
@@ -167,14 +179,15 @@ class Controller {
167
179
  return this._glModelViewMatrix(modelViewTransform, targetIndex);
168
180
  }
169
181
  async _detectAndMatch(inputData, targetIndexes) {
170
- const { featurePoints } = this.cropDetector.detectMoving(inputData);
182
+ const cropFeature = this.featureManager.getFeature("crop-detection");
183
+ const { featurePoints } = cropFeature.detect(inputData, true);
171
184
  const { targetIndex: matchedTargetIndex, modelViewTransform } = await this._workerMatch(featurePoints, targetIndexes);
172
185
  return { targetIndex: matchedTargetIndex, modelViewTransform };
173
186
  }
174
187
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
175
188
  const { worldCoords, screenCoords } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
176
- if (worldCoords.length < 10)
177
- return null; // Increased from 6 to 10 for better noise rejection
189
+ if (worldCoords.length < 8)
190
+ return null; // Resynced with Matcher (8 points) to allow initial detection
178
191
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
179
192
  worldCoords,
180
193
  screenCoords,
@@ -193,7 +206,6 @@ class Controller {
193
206
  currentModelViewTransform: null,
194
207
  trackCount: 0,
195
208
  trackMiss: 0,
196
- filter: new OneEuroFilter({ minCutOff: this.filterMinCF, beta: this.filterBeta }),
197
209
  });
198
210
  }
199
211
  const startProcessing = async () => {
@@ -229,43 +241,29 @@ class Controller {
229
241
  trackingState.currentModelViewTransform = modelViewTransform;
230
242
  }
231
243
  }
232
- if (!trackingState.showing) {
233
- if (trackingState.isTracking) {
234
- trackingState.trackMiss = 0;
235
- trackingState.trackCount += 1;
236
- if (trackingState.trackCount > this.warmupTolerance) {
237
- trackingState.showing = true;
238
- trackingState.trackingMatrix = null;
239
- trackingState.filter.reset();
240
- }
241
- }
242
- }
243
- if (trackingState.showing) {
244
- if (!trackingState.isTracking) {
245
- trackingState.trackCount = 0;
246
- trackingState.trackMiss += 1;
247
- if (trackingState.trackMiss > this.missTolerance) {
248
- trackingState.showing = false;
249
- trackingState.trackingMatrix = null;
250
- this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
251
- }
252
- }
253
- else {
254
- trackingState.trackMiss = 0;
255
- }
244
+ const wasShowing = trackingState.showing;
245
+ trackingState.showing = this.featureManager.shouldShow(i, trackingState.isTracking);
246
+ if (wasShowing && !trackingState.showing) {
247
+ trackingState.trackingMatrix = null;
248
+ this.onUpdate && this.onUpdate({ type: "updateMatrix", targetIndex: i, worldMatrix: null });
249
+ this.featureManager.notifyUpdate({ type: "reset", targetIndex: i });
256
250
  }
257
251
  if (trackingState.showing) {
258
252
  const worldMatrix = this._glModelViewMatrix(trackingState.currentModelViewTransform, i);
259
- trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
260
- let clone = [...trackingState.trackingMatrix];
253
+ const filteredMatrix = this.featureManager.applyWorldMatrixFilters(i, worldMatrix);
254
+ trackingState.trackingMatrix = filteredMatrix;
255
+ let finalMatrix = [...filteredMatrix];
261
256
  const isInputRotated = input.width === this.inputHeight && input.height === this.inputWidth;
262
257
  if (isInputRotated) {
263
- clone = this.getRotatedZ90Matrix(clone);
258
+ const rotationFeature = this.featureManager.getFeature("auto-rotation");
259
+ if (rotationFeature) {
260
+ finalMatrix = rotationFeature.rotate(finalMatrix);
261
+ }
264
262
  }
265
263
  this.onUpdate && this.onUpdate({
266
264
  type: "updateMatrix",
267
265
  targetIndex: i,
268
- worldMatrix: clone,
266
+ worldMatrix: finalMatrix,
269
267
  modelViewTransform: trackingState.currentModelViewTransform
270
268
  });
271
269
  }
@@ -286,7 +284,8 @@ class Controller {
286
284
  }
287
285
  async detect(input) {
288
286
  const inputData = this.inputLoader.loadInput(input);
289
- const { featurePoints, debugExtra } = this.cropDetector.detect(inputData);
287
+ const cropFeature = this.featureManager.getFeature("crop-detection");
288
+ const { featurePoints, debugExtra } = cropFeature.detect(inputData, false);
290
289
  return { featurePoints, debugExtra };
291
290
  }
292
291
  async match(featurePoints, targetIndex) {
@@ -22,6 +22,7 @@ export class CropDetector {
22
22
  projectedImage?: undefined;
23
23
  };
24
24
  };
25
+ frameCounter: number | undefined;
25
26
  _detectGlobal(imageData: any): {
26
27
  featurePoints: any[];
27
28
  debugExtra: {
@@ -24,39 +24,36 @@ class CropDetector {
24
24
  }
25
25
  detectMoving(input) {
26
26
  const imageData = input;
27
- // 🚀 MOONSHOT: High Frequency Global Scan
28
- // Scan full screen every 2 frames when searching to guarantee instant recovery
29
- if (this.lastRandomIndex % 2 === 0) {
30
- this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
27
+ if (!this.frameCounter)
28
+ this.frameCounter = 0;
29
+ this.frameCounter++;
30
+ // Scan full screen every 2 frames
31
+ if (this.frameCounter % 2 === 0) {
31
32
  return this._detectGlobal(imageData);
32
33
  }
33
- // Local crops (25 grid)
34
+ // Local crops: ensure we visit every single cell
34
35
  const gridSize = 5;
35
- const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
36
- // ... rest of logic remains but we hit it less often because global scan is more successful
37
- const dx = idx % gridSize;
38
- const dy = Math.floor(idx / gridSize);
36
+ const dx = this.lastRandomIndex % gridSize;
37
+ const dy = Math.floor(this.lastRandomIndex / gridSize);
39
38
  const stepX = this.cropSize / 3;
40
39
  const stepY = this.cropSize / 3;
41
40
  let startY = Math.floor(this.height / 2 - this.cropSize / 2 + (dy - 2) * stepY);
42
41
  let startX = Math.floor(this.width / 2 - this.cropSize / 2 + (dx - 2) * stepX);
43
42
  startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
44
43
  startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
45
- this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
44
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % (gridSize * gridSize);
46
45
  return this._detect(imageData, startX, startY);
47
46
  }
48
47
  _detectGlobal(imageData) {
49
48
  const croppedData = new Float32Array(this.cropSize * this.cropSize);
50
49
  const scaleX = this.width / this.cropSize;
51
50
  const scaleY = this.height / this.cropSize;
52
- // Better sampling: avoid missing edges by jumping too much
51
+ // Use sharp sampling for better descriptors
53
52
  for (let y = 0; y < this.cropSize; y++) {
54
53
  const srcY = Math.floor(y * scaleY) * this.width;
55
54
  const dstY = y * this.cropSize;
56
55
  for (let x = 0; x < this.cropSize; x++) {
57
- // Average slightly to preserve gradients
58
- const sx = Math.floor(x * scaleX);
59
- croppedData[dstY + x] = (imageData[srcY + sx] + imageData[srcY + sx + 1]) * 0.5;
56
+ croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
60
57
  }
61
58
  }
62
59
  const { featurePoints } = this.detector.detect(croppedData);
@@ -0,0 +1,15 @@
1
+ import { ControllerFeature } from "./feature-base.js";
2
+ export declare class AutoRotationFeature implements ControllerFeature {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ enabled: boolean;
7
+ private inputWidth;
8
+ private inputHeight;
9
+ init(context: {
10
+ inputWidth: number;
11
+ inputHeight: number;
12
+ }): void;
13
+ filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[];
14
+ rotate(m: number[]): number[];
15
+ }
@@ -0,0 +1,30 @@
1
+ export class AutoRotationFeature {
2
+ id = "auto-rotation";
3
+ name = "Auto Rotation Matrix";
4
+ description = "Automatically adjusts the world matrix if the input video is rotated (e.g. portrait mode).";
5
+ enabled = true;
6
+ inputWidth = 0;
7
+ inputHeight = 0;
8
+ init(context) {
9
+ this.inputWidth = context.inputWidth;
10
+ this.inputHeight = context.inputHeight;
11
+ }
12
+ filterWorldMatrix(targetIndex, worldMatrix) {
13
+ if (!this.enabled)
14
+ return worldMatrix;
15
+ // Check if input is rotated (this logic might need the actual current input dimensions)
16
+ // For now, we'll assume the controller passes the 'isRotated' info or we detect it
17
+ // But since this is a matrix post-process, we can just apply it if needed.
18
+ return worldMatrix;
19
+ }
20
+ // We might need a way to pass the 'currentInput' to the feature.
21
+ // Actually, the controller can just call this if it detects rotation.
22
+ rotate(m) {
23
+ return [
24
+ -m[1], m[0], m[2], m[3],
25
+ -m[5], m[4], m[6], m[7],
26
+ -m[9], m[8], m[10], m[11],
27
+ -m[13], m[12], m[14], m[15],
28
+ ];
29
+ }
30
+ }
@@ -0,0 +1,18 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+ export declare class CropDetectionFeature implements ControllerFeature {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ enabled: boolean;
7
+ private cropDetector;
8
+ private debugMode;
9
+ init(context: FeatureContext): void;
10
+ detect(inputData: any, isMoving?: boolean): {
11
+ featurePoints: any[];
12
+ debugExtra: {
13
+ projectedImage: number[];
14
+ } | {
15
+ projectedImage?: undefined;
16
+ };
17
+ };
18
+ }
@@ -0,0 +1,26 @@
1
+ import { CropDetector } from "../detector/crop-detector.js";
2
+ export class CropDetectionFeature {
3
+ id = "crop-detection";
4
+ name = "Crop Detection";
5
+ description = "Optimizes detection by focusing on areas with motion, reducing CPU usage.";
6
+ enabled = true;
7
+ cropDetector = null;
8
+ debugMode = false;
9
+ init(context) {
10
+ this.debugMode = context.debugMode;
11
+ this.cropDetector = new CropDetector(context.inputWidth, context.inputHeight, this.debugMode);
12
+ }
13
+ detect(inputData, isMoving = true) {
14
+ if (!this.enabled || !this.cropDetector) {
15
+ // Fallback to full detection if disabled?
16
+ // Actually CropDetector.detect is just full detection.
17
+ // We'll expose the methods here.
18
+ }
19
+ if (isMoving && this.enabled) {
20
+ return this.cropDetector.detectMoving(inputData);
21
+ }
22
+ else {
23
+ return this.cropDetector.detect(inputData);
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,46 @@
1
+ export interface FeatureContext {
2
+ inputWidth: number;
3
+ inputHeight: number;
4
+ projectionTransform: number[][];
5
+ debugMode: boolean;
6
+ }
7
+ export interface Feature {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ enabled: boolean;
12
+ init?(context: FeatureContext): void;
13
+ onUpdate?(data: any): void;
14
+ dispose?(): void;
15
+ }
16
+ /**
17
+ * Controller feature interface for processing hooks
18
+ */
19
+ export interface ControllerFeature extends Feature {
20
+ /**
21
+ * Called before processing a frame
22
+ */
23
+ beforeProcess?(inputData: any): void;
24
+ /**
25
+ * Called after detection/matching
26
+ */
27
+ afterMatch?(result: {
28
+ targetIndex: number;
29
+ modelViewTransform: number[][] | null;
30
+ }): void;
31
+ /**
32
+ * Called after tracking update
33
+ */
34
+ afterTrack?(result: {
35
+ targetIndex: number;
36
+ modelViewTransform: number[][] | null;
37
+ }): void;
38
+ /**
39
+ * Hook to filter or modify the final world matrix
40
+ */
41
+ filterWorldMatrix?(targetIndex: number, worldMatrix: number[]): number[];
42
+ /**
43
+ * Hook to decide if a target should be shown
44
+ */
45
+ shouldShow?(targetIndex: number, isTracking: boolean): boolean;
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+ export declare class FeatureManager {
3
+ private features;
4
+ addFeature(feature: ControllerFeature): void;
5
+ getFeature<T extends ControllerFeature>(id: string): T | undefined;
6
+ init(context: FeatureContext): void;
7
+ beforeProcess(inputData: any): void;
8
+ applyWorldMatrixFilters(targetIndex: number, worldMatrix: number[]): number[];
9
+ shouldShow(targetIndex: number, isTracking: boolean): boolean;
10
+ notifyUpdate(data: any): void;
11
+ dispose(): void;
12
+ }
@@ -0,0 +1,55 @@
1
+ export class FeatureManager {
2
+ features = [];
3
+ addFeature(feature) {
4
+ this.features.push(feature);
5
+ }
6
+ getFeature(id) {
7
+ return this.features.find(f => f.id === id);
8
+ }
9
+ init(context) {
10
+ for (const feature of this.features) {
11
+ if (feature.enabled && feature.init) {
12
+ feature.init(context);
13
+ }
14
+ }
15
+ }
16
+ beforeProcess(inputData) {
17
+ for (const feature of this.features) {
18
+ if (feature.enabled && feature.beforeProcess) {
19
+ feature.beforeProcess(inputData);
20
+ }
21
+ }
22
+ }
23
+ applyWorldMatrixFilters(targetIndex, worldMatrix) {
24
+ let result = worldMatrix;
25
+ for (const feature of this.features) {
26
+ if (feature.enabled && feature.filterWorldMatrix) {
27
+ result = feature.filterWorldMatrix(targetIndex, result);
28
+ }
29
+ }
30
+ return result;
31
+ }
32
+ shouldShow(targetIndex, isTracking) {
33
+ let show = isTracking;
34
+ for (const feature of this.features) {
35
+ if (feature.enabled && feature.shouldShow) {
36
+ show = feature.shouldShow(targetIndex, isTracking);
37
+ }
38
+ }
39
+ return show;
40
+ }
41
+ notifyUpdate(data) {
42
+ for (const feature of this.features) {
43
+ if (feature.enabled && feature.onUpdate) {
44
+ feature.onUpdate(data);
45
+ }
46
+ }
47
+ }
48
+ dispose() {
49
+ for (const feature of this.features) {
50
+ if (feature.dispose) {
51
+ feature.dispose();
52
+ }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,15 @@
1
+ import { ControllerFeature, FeatureContext } from "./feature-base.js";
2
+ export declare class OneEuroFilterFeature implements ControllerFeature {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ enabled: boolean;
7
+ private filters;
8
+ private minCutOff;
9
+ private beta;
10
+ constructor(minCutOff?: number, beta?: number);
11
+ init(context: FeatureContext): void;
12
+ private getFilter;
13
+ filterWorldMatrix(targetIndex: number, worldMatrix: number[]): number[];
14
+ onUpdate(data: any): void;
15
+ }
@@ -0,0 +1,37 @@
1
+ import { OneEuroFilter } from "../../libs/one-euro-filter.js";
2
+ export class OneEuroFilterFeature {
3
+ id = "one-euro-filter";
4
+ name = "One Euro Filter";
5
+ description = "Smooths the tracking matrix to reduce jitter using a One Euro Filter.";
6
+ enabled = true;
7
+ filters = [];
8
+ minCutOff;
9
+ beta;
10
+ constructor(minCutOff = 0.5, beta = 0.1) {
11
+ this.minCutOff = minCutOff;
12
+ this.beta = beta;
13
+ }
14
+ init(context) {
15
+ // We'll initialize filters lazily or based on target count if known
16
+ }
17
+ getFilter(targetIndex) {
18
+ if (!this.filters[targetIndex]) {
19
+ this.filters[targetIndex] = new OneEuroFilter({
20
+ minCutOff: this.minCutOff,
21
+ beta: this.beta
22
+ });
23
+ }
24
+ return this.filters[targetIndex];
25
+ }
26
+ filterWorldMatrix(targetIndex, worldMatrix) {
27
+ if (!this.enabled)
28
+ return worldMatrix;
29
+ const filter = this.getFilter(targetIndex);
30
+ return filter.filter(Date.now(), worldMatrix);
31
+ }
32
+ onUpdate(data) {
33
+ if (data.type === "reset" && data.targetIndex !== undefined) {
34
+ this.filters[data.targetIndex]?.reset();
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ import { ControllerFeature } from "./feature-base.js";
2
+ export interface TemporalState {
3
+ showing: boolean;
4
+ trackCount: number;
5
+ trackMiss: number;
6
+ }
7
+ export declare class TemporalFilterFeature implements ControllerFeature {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ enabled: boolean;
12
+ private states;
13
+ private warmupTolerance;
14
+ private missTolerance;
15
+ private onToggleShowing?;
16
+ constructor(warmup?: number, miss?: number, onToggleShowing?: (targetIndex: number, showing: boolean) => void);
17
+ private getState;
18
+ shouldShow(targetIndex: number, isTracking: boolean): boolean;
19
+ }
@@ -0,0 +1,57 @@
1
+ export class TemporalFilterFeature {
2
+ id = "temporal-filter";
3
+ name = "Temporal Filter";
4
+ description = "Provides warmup tolerance (to avoid false positives) and miss tolerance (to maintain tracking during brief occlusions).";
5
+ enabled = true;
6
+ states = [];
7
+ warmupTolerance;
8
+ missTolerance;
9
+ onToggleShowing;
10
+ constructor(warmup = 2, miss = 5, onToggleShowing) {
11
+ this.warmupTolerance = warmup;
12
+ this.missTolerance = miss;
13
+ this.onToggleShowing = onToggleShowing;
14
+ }
15
+ getState(targetIndex) {
16
+ if (!this.states[targetIndex]) {
17
+ this.states[targetIndex] = {
18
+ showing: false,
19
+ trackCount: 0,
20
+ trackMiss: 0,
21
+ };
22
+ }
23
+ return this.states[targetIndex];
24
+ }
25
+ shouldShow(targetIndex, isTracking) {
26
+ if (!this.enabled)
27
+ return isTracking;
28
+ const state = this.getState(targetIndex);
29
+ if (!state.showing) {
30
+ if (isTracking) {
31
+ state.trackMiss = 0;
32
+ state.trackCount += 1;
33
+ if (state.trackCount > this.warmupTolerance) {
34
+ state.showing = true;
35
+ this.onToggleShowing?.(targetIndex, true);
36
+ }
37
+ }
38
+ else {
39
+ state.trackCount = 0;
40
+ }
41
+ }
42
+ else {
43
+ if (!isTracking) {
44
+ state.trackCount = 0;
45
+ state.trackMiss += 1;
46
+ if (state.trackMiss > this.missTolerance) {
47
+ state.showing = false;
48
+ this.onToggleShowing?.(targetIndex, false);
49
+ }
50
+ }
51
+ else {
52
+ state.trackMiss = 0;
53
+ }
54
+ }
55
+ return state.showing;
56
+ }
57
+ }
@@ -95,7 +95,7 @@ class Tracker {
95
95
  }
96
96
  }
97
97
  // 2.1 Spatial distribution check: Avoid getting stuck in corners/noise
98
- if (screenCoords.length >= 10) {
98
+ if (screenCoords.length >= 8) {
99
99
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
100
100
  for (const p of screenCoords) {
101
101
  if (p.x < minX)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 { OneEuroFilter } from "../libs/one-euro-filter.js";
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
- this.filterMinCF = filterMinCF === null ? DEFAULT_FILTER_CUTOFF : filterMinCF;
82
- this.filterBeta = filterBeta === null ? DEFAULT_FILTER_BETA : filterBeta;
83
- this.warmupTolerance = warmupTolerance === null ? DEFAULT_WARMUP_TOLERANCE : warmupTolerance;
84
- this.missTolerance = missTolerance === null ? DEFAULT_MISS_TOLERANCE : missTolerance;
85
- this.cropDetector = new CropDetector(this.inputWidth, this.inputHeight, debugMode);
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.cropDetector.detect(inputData);
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 { featurePoints } = this.cropDetector.detectMoving(inputData);
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,
@@ -233,7 +257,7 @@ class Controller {
233
257
  lastModelViewTransform,
234
258
  targetIndex,
235
259
  );
236
- if (worldCoords.length < 10) return null; // Increased from 6 to 10 for better noise rejection
260
+ if (worldCoords.length < 8) return null; // Resynced with Matcher (8 points) to allow initial detection
237
261
  const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
238
262
  worldCoords,
239
263
  screenCoords,
@@ -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
- if (!trackingState.showing) {
302
- if (trackingState.isTracking) {
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
- if (!trackingState.isTracking) {
315
- trackingState.trackCount = 0;
316
- trackingState.trackMiss += 1;
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
- trackingState.trackingMatrix = trackingState.filter.filter(Date.now(), worldMatrix);
330
- let clone = [...trackingState.trackingMatrix];
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
- clone = this.getRotatedZ90Matrix(clone);
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: clone,
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 { featurePoints, debugExtra } = this.cropDetector.detect(inputData);
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
 
@@ -32,20 +32,19 @@ class CropDetector {
32
32
 
33
33
  detectMoving(input) {
34
34
  const imageData = input;
35
+ if (!this.frameCounter) this.frameCounter = 0;
36
+ this.frameCounter++;
35
37
 
36
- // 🚀 MOONSHOT: High Frequency Global Scan
37
- // Scan full screen every 2 frames when searching to guarantee instant recovery
38
- if (this.lastRandomIndex % 2 === 0) {
39
- this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
38
+ // Scan full screen every 2 frames
39
+ if (this.frameCounter % 2 === 0) {
40
40
  return this._detectGlobal(imageData);
41
41
  }
42
42
 
43
- // Local crops (25 grid)
43
+ // Local crops: ensure we visit every single cell
44
44
  const gridSize = 5;
45
- const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
46
- // ... rest of logic remains but we hit it less often because global scan is more successful
47
- const dx = idx % gridSize;
48
- const dy = Math.floor(idx / gridSize);
45
+ const dx = this.lastRandomIndex % gridSize;
46
+ const dy = Math.floor(this.lastRandomIndex / gridSize);
47
+
49
48
  const stepX = this.cropSize / 3;
50
49
  const stepY = this.cropSize / 3;
51
50
 
@@ -55,7 +54,7 @@ class CropDetector {
55
54
  startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
56
55
  startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
57
56
 
58
- this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
57
+ this.lastRandomIndex = (this.lastRandomIndex + 1) % (gridSize * gridSize);
59
58
  return this._detect(imageData, startX, startY);
60
59
  }
61
60
 
@@ -64,14 +63,12 @@ class CropDetector {
64
63
  const scaleX = this.width / this.cropSize;
65
64
  const scaleY = this.height / this.cropSize;
66
65
 
67
- // Better sampling: avoid missing edges by jumping too much
66
+ // Use sharp sampling for better descriptors
68
67
  for (let y = 0; y < this.cropSize; y++) {
69
68
  const srcY = Math.floor(y * scaleY) * this.width;
70
69
  const dstY = y * this.cropSize;
71
70
  for (let x = 0; x < this.cropSize; x++) {
72
- // Average slightly to preserve gradients
73
- const sx = Math.floor(x * scaleX);
74
- croppedData[dstY + x] = (imageData[srcY + sx] + imageData[srcY + sx + 1]) * 0.5;
71
+ croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
75
72
  }
76
73
  }
77
74
 
@@ -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
+ }
@@ -0,0 +1,68 @@
1
+ import { ControllerFeature } from "./feature-base.js";
2
+
3
+ export interface TemporalState {
4
+ showing: boolean;
5
+ trackCount: number;
6
+ trackMiss: number;
7
+ }
8
+
9
+ export class TemporalFilterFeature implements ControllerFeature {
10
+ id = "temporal-filter";
11
+ name = "Temporal Filter";
12
+ description = "Provides warmup tolerance (to avoid false positives) and miss tolerance (to maintain tracking during brief occlusions).";
13
+ enabled = true;
14
+
15
+ private states: TemporalState[] = [];
16
+ private warmupTolerance: number;
17
+ private missTolerance: number;
18
+ private onToggleShowing?: (targetIndex: number, showing: boolean) => void;
19
+
20
+ constructor(warmup: number = 2, miss: number = 5, onToggleShowing?: (targetIndex: number, showing: boolean) => void) {
21
+ this.warmupTolerance = warmup;
22
+ this.missTolerance = miss;
23
+ this.onToggleShowing = onToggleShowing;
24
+ }
25
+
26
+ private getState(targetIndex: number): TemporalState {
27
+ if (!this.states[targetIndex]) {
28
+ this.states[targetIndex] = {
29
+ showing: false,
30
+ trackCount: 0,
31
+ trackMiss: 0,
32
+ };
33
+ }
34
+ return this.states[targetIndex];
35
+ }
36
+
37
+ shouldShow(targetIndex: number, isTracking: boolean): boolean {
38
+ if (!this.enabled) return isTracking;
39
+
40
+ const state = this.getState(targetIndex);
41
+
42
+ if (!state.showing) {
43
+ if (isTracking) {
44
+ state.trackMiss = 0;
45
+ state.trackCount += 1;
46
+ if (state.trackCount > this.warmupTolerance) {
47
+ state.showing = true;
48
+ this.onToggleShowing?.(targetIndex, true);
49
+ }
50
+ } else {
51
+ state.trackCount = 0;
52
+ }
53
+ } else {
54
+ if (!isTracking) {
55
+ state.trackCount = 0;
56
+ state.trackMiss += 1;
57
+ if (state.trackMiss > this.missTolerance) {
58
+ state.showing = false;
59
+ this.onToggleShowing?.(targetIndex, false);
60
+ }
61
+ } else {
62
+ state.trackMiss = 0;
63
+ }
64
+ }
65
+
66
+ return state.showing;
67
+ }
68
+ }
@@ -137,7 +137,7 @@ class Tracker {
137
137
  }
138
138
 
139
139
  // 2.1 Spatial distribution check: Avoid getting stuck in corners/noise
140
- if (screenCoords.length >= 10) {
140
+ if (screenCoords.length >= 8) {
141
141
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
142
142
  for (const p of screenCoords) {
143
143
  if (p.x < minX) minX = p.x; if (p.y < minY) minY = p.y;