@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.
- package/dist/compiler/controller.d.ts +2 -6
- package/dist/compiler/controller.js +45 -46
- package/dist/compiler/detector/crop-detector.d.ts +1 -0
- package/dist/compiler/detector/crop-detector.js +11 -14
- 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/tracker/tracker.js +1 -1
- package/package.json +1 -1
- package/src/compiler/FEATURES.md +40 -0
- package/src/compiler/controller.ts +55 -43
- package/src/compiler/detector/crop-detector.js +11 -14
- 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/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 {
|
|
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.
|
|
55
|
-
this.
|
|
56
|
-
this.warmupTolerance
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
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.
|
|
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
|
|
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 <
|
|
177
|
-
return null; //
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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) {
|
|
@@ -24,39 +24,36 @@ class CropDetector {
|
|
|
24
24
|
}
|
|
25
25
|
detectMoving(input) {
|
|
26
26
|
const imageData = input;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
34
|
+
// Local crops: ensure we visit every single cell
|
|
34
35
|
const gridSize = 5;
|
|
35
|
-
const
|
|
36
|
-
|
|
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) %
|
|
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
|
-
//
|
|
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
|
-
|
|
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 >=
|
|
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
|
@@ -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,
|
|
@@ -233,7 +257,7 @@ class Controller {
|
|
|
233
257
|
lastModelViewTransform,
|
|
234
258
|
targetIndex,
|
|
235
259
|
);
|
|
236
|
-
if (worldCoords.length <
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
//
|
|
37
|
-
|
|
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
|
|
43
|
+
// Local crops: ensure we visit every single cell
|
|
44
44
|
const gridSize = 5;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
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) %
|
|
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
|
-
//
|
|
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
|
-
|
|
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 >=
|
|
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;
|