@srsergio/taptapp-ar 1.0.34 → 1.0.36
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.js +3 -3
- package/dist/compiler/offline-compiler.js +2 -2
- package/dist/compiler/simple-ar.d.ts +8 -1
- package/dist/compiler/simple-ar.js +112 -38
- package/dist/compiler/tracker/tracker.d.ts +1 -9
- package/dist/compiler/tracker/tracker.js +27 -12
- package/package.json +1 -1
- package/src/compiler/controller.js +2 -2
- package/src/compiler/offline-compiler.js +2 -2
- package/src/compiler/simple-ar.js +112 -42
- package/src/compiler/tracker/tracker.js +30 -13
|
@@ -16,7 +16,7 @@ catch (e) {
|
|
|
16
16
|
const DEFAULT_FILTER_CUTOFF = 0.1; // Menor cutoff para filtrar más ruidos cuando está quieto
|
|
17
17
|
const DEFAULT_FILTER_BETA = 0.01; // Beta bajo para suavizar movimientos rápidos
|
|
18
18
|
const DEFAULT_WARMUP_TOLERANCE = 8; // Más frames de calentamiento para asegurar estabilidad inicial
|
|
19
|
-
const DEFAULT_MISS_TOLERANCE =
|
|
19
|
+
const DEFAULT_MISS_TOLERANCE = 2; // Reducido para que el objeto desaparezca más rápido tras pérdida
|
|
20
20
|
class Controller {
|
|
21
21
|
constructor({ inputWidth, inputHeight, onUpdate = null, debugMode = false, maxTrack = 1, warmupTolerance = null, missTolerance = null, filterMinCF = null, filterBeta = null, worker = null, // Allow custom worker injection
|
|
22
22
|
}) {
|
|
@@ -182,8 +182,8 @@ class Controller {
|
|
|
182
182
|
}
|
|
183
183
|
async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
|
|
184
184
|
const { worldCoords, screenCoords } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
|
|
185
|
-
if (worldCoords.length <
|
|
186
|
-
return null;
|
|
185
|
+
if (worldCoords.length < 6)
|
|
186
|
+
return null; // Umbral de puntos mínimos para mantener el seguimiento
|
|
187
187
|
const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
|
|
188
188
|
worldCoords,
|
|
189
189
|
screenCoords,
|
|
@@ -217,9 +217,9 @@ export class OfflineCompiler {
|
|
|
217
217
|
}
|
|
218
218
|
const dataList = this.data.map((item) => {
|
|
219
219
|
const matchingData = item.matchingData.map((kf) => this._packKeyframe(kf));
|
|
220
|
-
const trackingData =
|
|
220
|
+
const trackingData = item.trackingData.map((td) => {
|
|
221
221
|
const count = td.points.length;
|
|
222
|
-
//
|
|
222
|
+
// Packed Coords - Float32 for now as in current import logic
|
|
223
223
|
const px = new Float32Array(count);
|
|
224
224
|
const py = new Float32Array(count);
|
|
225
225
|
for (let i = 0; i < count; i++) {
|
|
@@ -26,7 +26,7 @@ export class SimpleAR {
|
|
|
26
26
|
* @param {((data: {targetIndex: number, worldMatrix: number[]}) => void) | null} [options.onUpdate]
|
|
27
27
|
* @param {Object} [options.cameraConfig]
|
|
28
28
|
*/
|
|
29
|
-
constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, }: {
|
|
29
|
+
constructor({ container, targetSrc, overlay, scale, onFound, onLost, onUpdate, cameraConfig, debug, }: {
|
|
30
30
|
container: HTMLElement;
|
|
31
31
|
targetSrc: string | string[];
|
|
32
32
|
overlay: HTMLElement;
|
|
@@ -58,6 +58,11 @@ export class SimpleAR {
|
|
|
58
58
|
worldMatrix: number[];
|
|
59
59
|
}) => void) | null;
|
|
60
60
|
cameraConfig: Object;
|
|
61
|
+
debug: any;
|
|
62
|
+
lastTime: number;
|
|
63
|
+
frameCount: number;
|
|
64
|
+
fps: number;
|
|
65
|
+
debugPanel: HTMLDivElement | null;
|
|
61
66
|
video: HTMLVideoElement | null;
|
|
62
67
|
controller: Controller | null;
|
|
63
68
|
isTracking: boolean;
|
|
@@ -77,5 +82,7 @@ export class SimpleAR {
|
|
|
77
82
|
_initController(): void;
|
|
78
83
|
_handleUpdate(data: any): void;
|
|
79
84
|
_positionOverlay(mVT: any, targetIndex: any): void;
|
|
85
|
+
_createDebugPanel(): void;
|
|
86
|
+
_updateDebugPanel(isTracking: any): void;
|
|
80
87
|
}
|
|
81
88
|
import { Controller } from "./controller.js";
|
|
@@ -30,7 +30,7 @@ class SimpleAR {
|
|
|
30
30
|
* @param {Object} [options.cameraConfig]
|
|
31
31
|
*/
|
|
32
32
|
constructor({ container, targetSrc, overlay, scale = 1.0, // Multiplicador de escala personalizado
|
|
33
|
-
onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, }) {
|
|
33
|
+
onFound = null, onLost = null, onUpdate = null, cameraConfig = { facingMode: 'environment', width: 1280, height: 720 }, debug = false, }) {
|
|
34
34
|
this.container = container;
|
|
35
35
|
this.targetSrc = targetSrc;
|
|
36
36
|
this.overlay = overlay;
|
|
@@ -39,6 +39,13 @@ class SimpleAR {
|
|
|
39
39
|
this.onLost = onLost;
|
|
40
40
|
this.onUpdateCallback = onUpdate;
|
|
41
41
|
this.cameraConfig = cameraConfig;
|
|
42
|
+
this.debug = debug;
|
|
43
|
+
if (this.debug)
|
|
44
|
+
window.AR_DEBUG = true;
|
|
45
|
+
this.lastTime = performance.now();
|
|
46
|
+
this.frameCount = 0;
|
|
47
|
+
this.fps = 0;
|
|
48
|
+
this.debugPanel = null;
|
|
42
49
|
this.video = null;
|
|
43
50
|
this.controller = null;
|
|
44
51
|
this.isTracking = false;
|
|
@@ -55,6 +62,8 @@ class SimpleAR {
|
|
|
55
62
|
await this._startCamera();
|
|
56
63
|
// 3. Initialize controller
|
|
57
64
|
this._initController();
|
|
65
|
+
if (this.debug)
|
|
66
|
+
this._createDebugPanel();
|
|
58
67
|
// 4. Load targets (supports single URL or array of URLs)
|
|
59
68
|
const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
|
|
60
69
|
const result = await this.controller.addImageTargets(targets);
|
|
@@ -114,12 +123,23 @@ class SimpleAR {
|
|
|
114
123
|
this.controller = new Controller({
|
|
115
124
|
inputWidth: this.video.videoWidth,
|
|
116
125
|
inputHeight: this.video.videoHeight,
|
|
126
|
+
debugMode: this.debug,
|
|
117
127
|
onUpdate: (data) => this._handleUpdate(data)
|
|
118
128
|
});
|
|
119
129
|
}
|
|
120
130
|
_handleUpdate(data) {
|
|
121
131
|
if (data.type !== 'updateMatrix')
|
|
122
132
|
return;
|
|
133
|
+
// FPS Calculation
|
|
134
|
+
const now = performance.now();
|
|
135
|
+
this.frameCount++;
|
|
136
|
+
if (now - this.lastTime >= 1000) {
|
|
137
|
+
this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
|
|
138
|
+
this.frameCount = 0;
|
|
139
|
+
this.lastTime = now;
|
|
140
|
+
if (this.debug)
|
|
141
|
+
this._updateDebugPanel(this.isTracking);
|
|
142
|
+
}
|
|
123
143
|
const { targetIndex, worldMatrix, modelViewTransform } = data;
|
|
124
144
|
if (worldMatrix) {
|
|
125
145
|
// Target found
|
|
@@ -170,50 +190,104 @@ class SimpleAR {
|
|
|
170
190
|
const isPortrait = containerRect.height > containerRect.width;
|
|
171
191
|
const isVideoLandscape = videoW > videoH;
|
|
172
192
|
const needsRotation = isPortrait && isVideoLandscape;
|
|
173
|
-
//
|
|
193
|
+
// 3. Get intrinsic projection from controller
|
|
174
194
|
const proj = this.controller.projectionTransform;
|
|
175
|
-
// 3. Project
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
// 3. Project 4 corners to determine a full 3D perspective (homography)
|
|
196
|
+
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
197
|
+
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
198
|
+
const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
199
|
+
const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
200
|
+
// Helper to solve for 2D Homography (maps 0..1 square to pUL, pUR, pLL, pLR)
|
|
201
|
+
const solveHomography = (w, h, p1, p2, p3, p4) => {
|
|
202
|
+
const x1 = p1.sx, y1 = p1.sy;
|
|
203
|
+
const x2 = p2.sx, y2 = p2.sy;
|
|
204
|
+
const x3 = p3.sx, y3 = p3.sy;
|
|
205
|
+
const x4 = p4.sx, y4 = p4.sy;
|
|
206
|
+
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
207
|
+
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
208
|
+
let a, b, c, d, e, f, g, h_coeff;
|
|
209
|
+
if (dx3 === 0 && dy3 === 0) {
|
|
210
|
+
a = x2 - x1;
|
|
211
|
+
b = x3 - x1;
|
|
212
|
+
c = x1;
|
|
213
|
+
d = y2 - y1;
|
|
214
|
+
e = y3 - y1;
|
|
215
|
+
f = y1;
|
|
216
|
+
g = 0;
|
|
217
|
+
h_coeff = 0;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const det = dx1 * dy2 - dx2 * dy1;
|
|
221
|
+
g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
222
|
+
h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
223
|
+
a = x2 - x1 + g * x2;
|
|
224
|
+
b = x3 - x1 + h_coeff * x3;
|
|
225
|
+
c = x1;
|
|
226
|
+
d = y2 - y1 + g * y2;
|
|
227
|
+
e = y3 - y1 + h_coeff * y3;
|
|
228
|
+
f = y1;
|
|
229
|
+
}
|
|
230
|
+
// This maps unit square (0..1) to the quadrilateral.
|
|
231
|
+
// We need to scale it by 1/w and 1/h to map (0..w, 0..h)
|
|
232
|
+
return [
|
|
233
|
+
a / w, d / w, 0, g / w,
|
|
234
|
+
b / h, e / h, 0, h_coeff / h,
|
|
235
|
+
0, 0, 1, 0,
|
|
236
|
+
c, f, 0, 1
|
|
237
|
+
];
|
|
238
|
+
};
|
|
239
|
+
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
240
|
+
// Apply styles
|
|
199
241
|
this.overlay.style.maxWidth = 'none';
|
|
200
|
-
this.overlay.style.maxHeight = 'none';
|
|
201
242
|
this.overlay.style.width = `${markerW}px`;
|
|
202
|
-
this.overlay.style.height =
|
|
243
|
+
this.overlay.style.height = `${markerH}px`;
|
|
203
244
|
this.overlay.style.position = 'absolute';
|
|
204
|
-
this.overlay.style.transformOrigin = '
|
|
205
|
-
this.overlay.style.display = 'block';
|
|
206
|
-
this.overlay.style.margin = '0';
|
|
245
|
+
this.overlay.style.transformOrigin = '0 0';
|
|
207
246
|
this.overlay.style.left = '0';
|
|
208
247
|
this.overlay.style.top = '0';
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
//
|
|
248
|
+
this.overlay.style.display = 'block';
|
|
249
|
+
// Apply 3D transform with matrix3d
|
|
250
|
+
// We also apply the user's custom scaleMultiplier AFTER the perspective transform
|
|
251
|
+
// but since we want to scale around the marker center, we apply it as a prefix/suffix
|
|
252
|
+
// Scale around top-left (0,0) is easy. Scale around center requires offset.
|
|
212
253
|
this.overlay.style.transform = `
|
|
213
|
-
|
|
214
|
-
translate(
|
|
215
|
-
|
|
216
|
-
|
|
254
|
+
matrix3d(${matrix.join(',')})
|
|
255
|
+
translate(${markerW / 2}px, ${markerH / 2}px)
|
|
256
|
+
scale(${this.scaleMultiplier})
|
|
257
|
+
translate(${-markerW / 2}px, ${-markerH / 2}px)
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
// Unified projection logic moved to ./utils/projection.js
|
|
261
|
+
_createDebugPanel() {
|
|
262
|
+
this.debugPanel = document.createElement('div');
|
|
263
|
+
this.debugPanel.style.cssText = `
|
|
264
|
+
position: absolute;
|
|
265
|
+
top: 10px;
|
|
266
|
+
left: 10px;
|
|
267
|
+
background: rgba(0, 0, 0, 0.8);
|
|
268
|
+
color: #0f0;
|
|
269
|
+
font-family: monospace;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
padding: 8px;
|
|
272
|
+
border-radius: 4px;
|
|
273
|
+
z-index: 99999;
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
line-height: 1.5;
|
|
276
|
+
`;
|
|
277
|
+
this.container.appendChild(this.debugPanel);
|
|
278
|
+
}
|
|
279
|
+
_updateDebugPanel(isTracking) {
|
|
280
|
+
if (!this.debugPanel)
|
|
281
|
+
return;
|
|
282
|
+
const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
|
|
283
|
+
const color = isTracking ? '#0f0' : '#f00';
|
|
284
|
+
const status = isTracking ? 'TRACKING' : 'SEARCHING';
|
|
285
|
+
this.debugPanel.innerHTML = `
|
|
286
|
+
<div>HEAD-UP DISPLAY</div>
|
|
287
|
+
<div>----------------</div>
|
|
288
|
+
<div>FPS: ${this.fps}</div>
|
|
289
|
+
<div>STATUS: <span style="color:${color}">${status}</span></div>
|
|
290
|
+
<div>MEM: ${memory} MB</div>
|
|
217
291
|
`;
|
|
218
292
|
}
|
|
219
293
|
}
|
|
@@ -7,15 +7,7 @@ export class Tracker {
|
|
|
7
7
|
inputHeight: any;
|
|
8
8
|
debugMode: boolean;
|
|
9
9
|
trackingKeyframeList: any[];
|
|
10
|
-
prebuiltData:
|
|
11
|
-
px: Float32Array<any>;
|
|
12
|
-
py: Float32Array<any>;
|
|
13
|
-
data: Uint8Array<any>;
|
|
14
|
-
width: any;
|
|
15
|
-
height: any;
|
|
16
|
-
scale: any;
|
|
17
|
-
projectedImage: Float32Array<ArrayBuffer>;
|
|
18
|
-
}[];
|
|
10
|
+
prebuiltData: any[];
|
|
19
11
|
templateBuffer: Float32Array<ArrayBuffer>;
|
|
20
12
|
dummyRun(inputData: any): void;
|
|
21
13
|
track(inputData: any, lastModelViewTransform: any, targetIndex: any): {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
|
|
2
2
|
const AR2_DEFAULT_TS = 6;
|
|
3
3
|
const AR2_DEFAULT_TS_GAP = 1;
|
|
4
|
-
const AR2_SEARCH_SIZE =
|
|
4
|
+
const AR2_SEARCH_SIZE = 18;
|
|
5
5
|
const AR2_SEARCH_GAP = 1;
|
|
6
6
|
const AR2_SIM_THRESH = 0.6;
|
|
7
7
|
const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
|
|
@@ -13,15 +13,12 @@ class Tracker {
|
|
|
13
13
|
this.inputWidth = inputWidth;
|
|
14
14
|
this.inputHeight = inputHeight;
|
|
15
15
|
this.debugMode = debugMode;
|
|
16
|
-
this.trackingKeyframeList = [];
|
|
16
|
+
this.trackingKeyframeList = []; // All octaves for all targets: [targetIndex][octaveIndex]
|
|
17
|
+
this.prebuiltData = []; // [targetIndex][octaveIndex]
|
|
17
18
|
for (let i = 0; i < trackingDataList.length; i++) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.prebuiltData = [];
|
|
22
|
-
for (let i = 0; i < this.trackingKeyframeList.length; i++) {
|
|
23
|
-
const keyframe = this.trackingKeyframeList[i];
|
|
24
|
-
this.prebuiltData[i] = {
|
|
19
|
+
const targetOctaves = trackingDataList[i];
|
|
20
|
+
this.trackingKeyframeList[i] = targetOctaves;
|
|
21
|
+
this.prebuiltData[i] = targetOctaves.map(keyframe => ({
|
|
25
22
|
px: new Float32Array(keyframe.px),
|
|
26
23
|
py: new Float32Array(keyframe.py),
|
|
27
24
|
data: new Uint8Array(keyframe.d),
|
|
@@ -30,7 +27,7 @@ class Tracker {
|
|
|
30
27
|
scale: keyframe.s,
|
|
31
28
|
// Recyclable projected image buffer
|
|
32
29
|
projectedImage: new Float32Array(keyframe.w * keyframe.h)
|
|
33
|
-
};
|
|
30
|
+
}));
|
|
34
31
|
}
|
|
35
32
|
// Pre-allocate template data buffer to avoid garbage collection
|
|
36
33
|
const templateOneSize = AR2_DEFAULT_TS;
|
|
@@ -49,14 +46,31 @@ class Tracker {
|
|
|
49
46
|
}
|
|
50
47
|
track(inputData, lastModelViewTransform, targetIndex) {
|
|
51
48
|
let debugExtra = {};
|
|
49
|
+
// Select the best octave based on current estimated distance/scale
|
|
50
|
+
// We want the octave where the marker size is closest to its projected size on screen
|
|
52
51
|
const modelViewProjectionTransform = buildModelViewProjectionTransform(this.projectionTransform, lastModelViewTransform);
|
|
53
|
-
|
|
52
|
+
// Estimate current marker width on screen
|
|
53
|
+
const [mW, mH] = this.markerDimensions[targetIndex];
|
|
54
|
+
const p0 = computeScreenCoordiate(modelViewProjectionTransform, 0, 0);
|
|
55
|
+
const p1 = computeScreenCoordiate(modelViewProjectionTransform, mW, 0);
|
|
56
|
+
const screenW = Math.sqrt((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2);
|
|
57
|
+
// Select octave whose image width is closest to screenW
|
|
58
|
+
let octaveIndex = 0;
|
|
59
|
+
let minDiff = Infinity;
|
|
60
|
+
for (let i = 0; i < this.prebuiltData[targetIndex].length; i++) {
|
|
61
|
+
const diff = Math.abs(this.prebuiltData[targetIndex][i].width - screenW);
|
|
62
|
+
if (diff < minDiff) {
|
|
63
|
+
minDiff = diff;
|
|
64
|
+
octaveIndex = i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
|
|
54
68
|
// 1. Compute Projection (Warping)
|
|
55
69
|
this._computeProjection(modelViewProjectionTransform, inputData, prebuilt);
|
|
56
70
|
const projectedImage = prebuilt.projectedImage;
|
|
57
71
|
// 2. Compute Matching (NCC)
|
|
58
72
|
const { matchingPoints, sim } = this._computeMatching(prebuilt, projectedImage);
|
|
59
|
-
const trackingFrame = this.trackingKeyframeList[targetIndex];
|
|
73
|
+
const trackingFrame = this.trackingKeyframeList[targetIndex][octaveIndex];
|
|
60
74
|
const worldCoords = [];
|
|
61
75
|
const screenCoords = [];
|
|
62
76
|
const goodTrack = [];
|
|
@@ -75,6 +89,7 @@ class Tracker {
|
|
|
75
89
|
}
|
|
76
90
|
if (this.debugMode) {
|
|
77
91
|
debugExtra = {
|
|
92
|
+
octaveIndex,
|
|
78
93
|
projectedImage: Array.from(projectedImage),
|
|
79
94
|
matchingPoints,
|
|
80
95
|
goodTrack,
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ const DEFAULT_FILTER_BETA = 0.01; // Beta bajo para suavizar movimientos rápid
|
|
|
20
20
|
|
|
21
21
|
const DEFAULT_WARMUP_TOLERANCE = 8; // Más frames de calentamiento para asegurar estabilidad inicial
|
|
22
22
|
|
|
23
|
-
const DEFAULT_MISS_TOLERANCE =
|
|
23
|
+
const DEFAULT_MISS_TOLERANCE = 2; // Reducido para que el objeto desaparezca más rápido tras pérdida
|
|
24
24
|
|
|
25
25
|
class Controller {
|
|
26
26
|
constructor({
|
|
@@ -231,7 +231,7 @@ class Controller {
|
|
|
231
231
|
lastModelViewTransform,
|
|
232
232
|
targetIndex,
|
|
233
233
|
);
|
|
234
|
-
if (worldCoords.length <
|
|
234
|
+
if (worldCoords.length < 6) return null; // Umbral de puntos mínimos para mantener el seguimiento
|
|
235
235
|
const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
|
|
236
236
|
worldCoords,
|
|
237
237
|
screenCoords,
|
|
@@ -260,9 +260,9 @@ export class OfflineCompiler {
|
|
|
260
260
|
const dataList = this.data.map((item) => {
|
|
261
261
|
const matchingData = item.matchingData.map((kf) => this._packKeyframe(kf));
|
|
262
262
|
|
|
263
|
-
const trackingData =
|
|
263
|
+
const trackingData = item.trackingData.map((td) => {
|
|
264
264
|
const count = td.points.length;
|
|
265
|
-
//
|
|
265
|
+
// Packed Coords - Float32 for now as in current import logic
|
|
266
266
|
const px = new Float32Array(count);
|
|
267
267
|
const py = new Float32Array(count);
|
|
268
268
|
for (let i = 0; i < count; i++) {
|
|
@@ -39,6 +39,7 @@ class SimpleAR {
|
|
|
39
39
|
onLost = null,
|
|
40
40
|
onUpdate = null,
|
|
41
41
|
cameraConfig = { facingMode: 'environment', width: 1280, height: 720 },
|
|
42
|
+
debug = false,
|
|
42
43
|
}) {
|
|
43
44
|
this.container = container;
|
|
44
45
|
this.targetSrc = targetSrc;
|
|
@@ -48,6 +49,13 @@ class SimpleAR {
|
|
|
48
49
|
this.onLost = onLost;
|
|
49
50
|
this.onUpdateCallback = onUpdate;
|
|
50
51
|
this.cameraConfig = cameraConfig;
|
|
52
|
+
this.debug = debug;
|
|
53
|
+
if (this.debug) window.AR_DEBUG = true;
|
|
54
|
+
|
|
55
|
+
this.lastTime = performance.now();
|
|
56
|
+
this.frameCount = 0;
|
|
57
|
+
this.fps = 0;
|
|
58
|
+
this.debugPanel = null;
|
|
51
59
|
|
|
52
60
|
this.video = null;
|
|
53
61
|
this.controller = null;
|
|
@@ -69,6 +77,8 @@ class SimpleAR {
|
|
|
69
77
|
// 3. Initialize controller
|
|
70
78
|
this._initController();
|
|
71
79
|
|
|
80
|
+
if (this.debug) this._createDebugPanel();
|
|
81
|
+
|
|
72
82
|
// 4. Load targets (supports single URL or array of URLs)
|
|
73
83
|
const targets = Array.isArray(this.targetSrc) ? this.targetSrc : [this.targetSrc];
|
|
74
84
|
const result = await this.controller.addImageTargets(targets);
|
|
@@ -134,6 +144,7 @@ class SimpleAR {
|
|
|
134
144
|
this.controller = new Controller({
|
|
135
145
|
inputWidth: this.video.videoWidth,
|
|
136
146
|
inputHeight: this.video.videoHeight,
|
|
147
|
+
debugMode: this.debug,
|
|
137
148
|
onUpdate: (data) => this._handleUpdate(data)
|
|
138
149
|
});
|
|
139
150
|
}
|
|
@@ -141,6 +152,16 @@ class SimpleAR {
|
|
|
141
152
|
_handleUpdate(data) {
|
|
142
153
|
if (data.type !== 'updateMatrix') return;
|
|
143
154
|
|
|
155
|
+
// FPS Calculation
|
|
156
|
+
const now = performance.now();
|
|
157
|
+
this.frameCount++;
|
|
158
|
+
if (now - this.lastTime >= 1000) {
|
|
159
|
+
this.fps = Math.round((this.frameCount * 1000) / (now - this.lastTime));
|
|
160
|
+
this.frameCount = 0;
|
|
161
|
+
this.lastTime = now;
|
|
162
|
+
if (this.debug) this._updateDebugPanel(this.isTracking);
|
|
163
|
+
}
|
|
164
|
+
|
|
144
165
|
const { targetIndex, worldMatrix, modelViewTransform } = data;
|
|
145
166
|
|
|
146
167
|
if (worldMatrix) {
|
|
@@ -198,62 +219,111 @@ class SimpleAR {
|
|
|
198
219
|
const isVideoLandscape = videoW > videoH;
|
|
199
220
|
const needsRotation = isPortrait && isVideoLandscape;
|
|
200
221
|
|
|
201
|
-
//
|
|
222
|
+
// 3. Get intrinsic projection from controller
|
|
202
223
|
const proj = this.controller.projectionTransform;
|
|
203
224
|
|
|
204
|
-
// 3. Project
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
// 3. Project 4 corners to determine a full 3D perspective (homography)
|
|
226
|
+
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
227
|
+
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
228
|
+
const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
229
|
+
const pLR = projectToScreen(markerW, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
230
|
+
|
|
231
|
+
// Helper to solve for 2D Homography (maps 0..1 square to pUL, pUR, pLL, pLR)
|
|
232
|
+
const solveHomography = (w, h, p1, p2, p3, p4) => {
|
|
233
|
+
const x1 = p1.sx, y1 = p1.sy;
|
|
234
|
+
const x2 = p2.sx, y2 = p2.sy;
|
|
235
|
+
const x3 = p3.sx, y3 = p3.sy;
|
|
236
|
+
const x4 = p4.sx, y4 = p4.sy;
|
|
237
|
+
|
|
238
|
+
const dx1 = x2 - x4, dx2 = x3 - x4, dx3 = x1 - x2 + x4 - x3;
|
|
239
|
+
const dy1 = y2 - y4, dy2 = y3 - y4, dy3 = y1 - y2 + y4 - y3;
|
|
240
|
+
|
|
241
|
+
let a, b, c, d, e, f, g, h_coeff;
|
|
242
|
+
|
|
243
|
+
if (dx3 === 0 && dy3 === 0) {
|
|
244
|
+
a = x2 - x1; b = x3 - x1; c = x1;
|
|
245
|
+
d = y2 - y1; e = y3 - y1; f = y1;
|
|
246
|
+
g = 0; h_coeff = 0;
|
|
247
|
+
} else {
|
|
248
|
+
const det = dx1 * dy2 - dx2 * dy1;
|
|
249
|
+
g = (dx3 * dy2 - dx2 * dy3) / det;
|
|
250
|
+
h_coeff = (dx1 * dy3 - dx3 * dy1) / det;
|
|
251
|
+
a = x2 - x1 + g * x2;
|
|
252
|
+
b = x3 - x1 + h_coeff * x3;
|
|
253
|
+
c = x1;
|
|
254
|
+
d = y2 - y1 + g * y2;
|
|
255
|
+
e = y3 - y1 + h_coeff * y3;
|
|
256
|
+
f = y1;
|
|
257
|
+
}
|
|
258
|
+
// This maps unit square (0..1) to the quadrilateral.
|
|
259
|
+
// We need to scale it by 1/w and 1/h to map (0..w, 0..h)
|
|
260
|
+
return [
|
|
261
|
+
a / w, d / w, 0, g / w,
|
|
262
|
+
b / h, e / h, 0, h_coeff / h,
|
|
263
|
+
0, 0, 1, 0,
|
|
264
|
+
c, f, 0, 1
|
|
265
|
+
];
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
232
269
|
|
|
233
|
-
// Apply styles
|
|
270
|
+
// Apply styles
|
|
234
271
|
this.overlay.style.maxWidth = 'none';
|
|
235
|
-
this.overlay.style.maxHeight = 'none';
|
|
236
272
|
this.overlay.style.width = `${markerW}px`;
|
|
237
|
-
this.overlay.style.height =
|
|
273
|
+
this.overlay.style.height = `${markerH}px`;
|
|
238
274
|
this.overlay.style.position = 'absolute';
|
|
239
|
-
this.overlay.style.transformOrigin = '
|
|
240
|
-
this.overlay.style.display = 'block';
|
|
241
|
-
this.overlay.style.margin = '0';
|
|
275
|
+
this.overlay.style.transformOrigin = '0 0';
|
|
242
276
|
this.overlay.style.left = '0';
|
|
243
277
|
this.overlay.style.top = '0';
|
|
278
|
+
this.overlay.style.display = 'block';
|
|
244
279
|
|
|
245
|
-
// Apply
|
|
246
|
-
// We
|
|
247
|
-
//
|
|
280
|
+
// Apply 3D transform with matrix3d
|
|
281
|
+
// We also apply the user's custom scaleMultiplier AFTER the perspective transform
|
|
282
|
+
// but since we want to scale around the marker center, we apply it as a prefix/suffix
|
|
283
|
+
// Scale around top-left (0,0) is easy. Scale around center requires offset.
|
|
248
284
|
this.overlay.style.transform = `
|
|
249
|
-
|
|
250
|
-
translate(
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
matrix3d(${matrix.join(',')})
|
|
286
|
+
translate(${markerW / 2}px, ${markerH / 2}px)
|
|
287
|
+
scale(${this.scaleMultiplier})
|
|
288
|
+
translate(${-markerW / 2}px, ${-markerH / 2}px)
|
|
253
289
|
`;
|
|
254
290
|
}
|
|
255
291
|
|
|
256
292
|
// Unified projection logic moved to ./utils/projection.js
|
|
293
|
+
|
|
294
|
+
_createDebugPanel() {
|
|
295
|
+
this.debugPanel = document.createElement('div');
|
|
296
|
+
this.debugPanel.style.cssText = `
|
|
297
|
+
position: absolute;
|
|
298
|
+
top: 10px;
|
|
299
|
+
left: 10px;
|
|
300
|
+
background: rgba(0, 0, 0, 0.8);
|
|
301
|
+
color: #0f0;
|
|
302
|
+
font-family: monospace;
|
|
303
|
+
font-size: 12px;
|
|
304
|
+
padding: 8px;
|
|
305
|
+
border-radius: 4px;
|
|
306
|
+
z-index: 99999;
|
|
307
|
+
pointer-events: none;
|
|
308
|
+
line-height: 1.5;
|
|
309
|
+
`;
|
|
310
|
+
this.container.appendChild(this.debugPanel);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_updateDebugPanel(isTracking) {
|
|
314
|
+
if (!this.debugPanel) return;
|
|
315
|
+
const memory = performance.memory ? Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) : '?';
|
|
316
|
+
const color = isTracking ? '#0f0' : '#f00';
|
|
317
|
+
const status = isTracking ? 'TRACKING' : 'SEARCHING';
|
|
318
|
+
|
|
319
|
+
this.debugPanel.innerHTML = `
|
|
320
|
+
<div>HEAD-UP DISPLAY</div>
|
|
321
|
+
<div>----------------</div>
|
|
322
|
+
<div>FPS: ${this.fps}</div>
|
|
323
|
+
<div>STATUS: <span style="color:${color}">${status}</span></div>
|
|
324
|
+
<div>MEM: ${memory} MB</div>
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
257
327
|
}
|
|
258
328
|
|
|
259
329
|
export { SimpleAR };
|
|
@@ -2,7 +2,7 @@ import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../es
|
|
|
2
2
|
|
|
3
3
|
const AR2_DEFAULT_TS = 6;
|
|
4
4
|
const AR2_DEFAULT_TS_GAP = 1;
|
|
5
|
-
const AR2_SEARCH_SIZE =
|
|
5
|
+
const AR2_SEARCH_SIZE = 18;
|
|
6
6
|
const AR2_SEARCH_GAP = 1;
|
|
7
7
|
const AR2_SIM_THRESH = 0.6;
|
|
8
8
|
|
|
@@ -24,16 +24,13 @@ class Tracker {
|
|
|
24
24
|
this.inputHeight = inputHeight;
|
|
25
25
|
this.debugMode = debugMode;
|
|
26
26
|
|
|
27
|
-
this.trackingKeyframeList = [];
|
|
28
|
-
|
|
29
|
-
this.trackingKeyframeList.push(trackingDataList[i][TRACKING_KEYFRAME]);
|
|
30
|
-
}
|
|
27
|
+
this.trackingKeyframeList = []; // All octaves for all targets: [targetIndex][octaveIndex]
|
|
28
|
+
this.prebuiltData = []; // [targetIndex][octaveIndex]
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.prebuiltData[i] = {
|
|
30
|
+
for (let i = 0; i < trackingDataList.length; i++) {
|
|
31
|
+
const targetOctaves = trackingDataList[i];
|
|
32
|
+
this.trackingKeyframeList[i] = targetOctaves;
|
|
33
|
+
this.prebuiltData[i] = targetOctaves.map(keyframe => ({
|
|
37
34
|
px: new Float32Array(keyframe.px),
|
|
38
35
|
py: new Float32Array(keyframe.py),
|
|
39
36
|
data: new Uint8Array(keyframe.d),
|
|
@@ -42,7 +39,7 @@ class Tracker {
|
|
|
42
39
|
scale: keyframe.s,
|
|
43
40
|
// Recyclable projected image buffer
|
|
44
41
|
projectedImage: new Float32Array(keyframe.w * keyframe.h)
|
|
45
|
-
};
|
|
42
|
+
}));
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
// Pre-allocate template data buffer to avoid garbage collection
|
|
@@ -65,12 +62,31 @@ class Tracker {
|
|
|
65
62
|
track(inputData, lastModelViewTransform, targetIndex) {
|
|
66
63
|
let debugExtra = {};
|
|
67
64
|
|
|
65
|
+
// Select the best octave based on current estimated distance/scale
|
|
66
|
+
// We want the octave where the marker size is closest to its projected size on screen
|
|
68
67
|
const modelViewProjectionTransform = buildModelViewProjectionTransform(
|
|
69
68
|
this.projectionTransform,
|
|
70
69
|
lastModelViewTransform,
|
|
71
70
|
);
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
// Estimate current marker width on screen
|
|
73
|
+
const [mW, mH] = this.markerDimensions[targetIndex];
|
|
74
|
+
const p0 = computeScreenCoordiate(modelViewProjectionTransform, 0, 0);
|
|
75
|
+
const p1 = computeScreenCoordiate(modelViewProjectionTransform, mW, 0);
|
|
76
|
+
const screenW = Math.sqrt((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2);
|
|
77
|
+
|
|
78
|
+
// Select octave whose image width is closest to screenW
|
|
79
|
+
let octaveIndex = 0;
|
|
80
|
+
let minDiff = Infinity;
|
|
81
|
+
for (let i = 0; i < this.prebuiltData[targetIndex].length; i++) {
|
|
82
|
+
const diff = Math.abs(this.prebuiltData[targetIndex][i].width - screenW);
|
|
83
|
+
if (diff < minDiff) {
|
|
84
|
+
minDiff = diff;
|
|
85
|
+
octaveIndex = i;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
|
|
74
90
|
|
|
75
91
|
// 1. Compute Projection (Warping)
|
|
76
92
|
this._computeProjection(
|
|
@@ -87,7 +103,7 @@ class Tracker {
|
|
|
87
103
|
projectedImage
|
|
88
104
|
);
|
|
89
105
|
|
|
90
|
-
const trackingFrame = this.trackingKeyframeList[targetIndex];
|
|
106
|
+
const trackingFrame = this.trackingKeyframeList[targetIndex][octaveIndex];
|
|
91
107
|
const worldCoords = [];
|
|
92
108
|
const screenCoords = [];
|
|
93
109
|
const goodTrack = [];
|
|
@@ -113,6 +129,7 @@ class Tracker {
|
|
|
113
129
|
|
|
114
130
|
if (this.debugMode) {
|
|
115
131
|
debugExtra = {
|
|
132
|
+
octaveIndex,
|
|
116
133
|
projectedImage: Array.from(projectedImage),
|
|
117
134
|
matchingPoints,
|
|
118
135
|
goodTrack,
|