@srsergio/taptapp-ar 1.0.35 → 1.0.37
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 +5 -1
- package/dist/compiler/controller.js +26 -5
- package/dist/compiler/offline-compiler.js +2 -2
- package/dist/compiler/simple-ar.js +51 -42
- package/dist/compiler/tracker/tracker.d.ts +8 -9
- package/dist/compiler/tracker/tracker.js +46 -11
- package/package.json +1 -1
- package/src/compiler/controller.js +25 -5
- package/src/compiler/offline-compiler.js +2 -2
- package/src/compiler/simple-ar.js +49 -50
- package/src/compiler/tracker/tracker.js +51 -12
|
@@ -70,7 +70,11 @@ export class Controller {
|
|
|
70
70
|
targetIndex: any;
|
|
71
71
|
modelViewTransform: any;
|
|
72
72
|
}>;
|
|
73
|
-
_trackAndUpdate(inputData: any, lastModelViewTransform: any, targetIndex: any): Promise<
|
|
73
|
+
_trackAndUpdate(inputData: any, lastModelViewTransform: any, targetIndex: any): Promise<{
|
|
74
|
+
modelViewTransform: any;
|
|
75
|
+
inliers: number;
|
|
76
|
+
octaveIndex: any;
|
|
77
|
+
} | null>;
|
|
74
78
|
processVideo(input: any): void;
|
|
75
79
|
stopProcessVideo(): void;
|
|
76
80
|
detect(input: any): Promise<{
|
|
@@ -181,14 +181,18 @@ class Controller {
|
|
|
181
181
|
return { targetIndex: matchedTargetIndex, modelViewTransform };
|
|
182
182
|
}
|
|
183
183
|
async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
|
|
184
|
-
const { worldCoords, screenCoords } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
|
|
184
|
+
const { worldCoords, screenCoords, debugExtra } = this.tracker.track(inputData, lastModelViewTransform, targetIndex);
|
|
185
185
|
if (worldCoords.length < 6)
|
|
186
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,
|
|
190
190
|
});
|
|
191
|
-
return
|
|
191
|
+
return {
|
|
192
|
+
modelViewTransform,
|
|
193
|
+
inliers: worldCoords.length,
|
|
194
|
+
octaveIndex: debugExtra.octaveIndex
|
|
195
|
+
};
|
|
192
196
|
}
|
|
193
197
|
processVideo(input) {
|
|
194
198
|
if (this.processingVideo)
|
|
@@ -202,6 +206,7 @@ class Controller {
|
|
|
202
206
|
currentModelViewTransform: null,
|
|
203
207
|
trackCount: 0,
|
|
204
208
|
trackMiss: 0,
|
|
209
|
+
stabilityCount: 0, // Nuevo: Contador para Live Adaptation
|
|
205
210
|
filter: new OneEuroFilter({ minCutOff: this.filterMinCF, beta: this.filterBeta }),
|
|
206
211
|
});
|
|
207
212
|
}
|
|
@@ -234,12 +239,28 @@ class Controller {
|
|
|
234
239
|
for (let i = 0; i < this.trackingStates.length; i++) {
|
|
235
240
|
const trackingState = this.trackingStates[i];
|
|
236
241
|
if (trackingState.isTracking) {
|
|
237
|
-
let
|
|
238
|
-
if (
|
|
242
|
+
let result = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
|
|
243
|
+
if (result === null) {
|
|
239
244
|
trackingState.isTracking = false;
|
|
245
|
+
trackingState.stabilityCount = 0;
|
|
240
246
|
}
|
|
241
247
|
else {
|
|
242
|
-
trackingState.currentModelViewTransform = modelViewTransform;
|
|
248
|
+
trackingState.currentModelViewTransform = result.modelViewTransform;
|
|
249
|
+
// --- LIVE MODEL ADAPTATION LOGIC ---
|
|
250
|
+
// Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
|
|
251
|
+
if (result.inliers > 25) {
|
|
252
|
+
trackingState.stabilityCount++;
|
|
253
|
+
if (trackingState.stabilityCount > 20) { // 20 frames de estabilidad absoluta
|
|
254
|
+
this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.1); // 10% de mezcla real
|
|
255
|
+
if (this.debugMode)
|
|
256
|
+
console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated with real-world textures.`);
|
|
257
|
+
trackingState.stabilityCount = 0; // Reset para la siguiente actualización
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
|
|
262
|
+
}
|
|
263
|
+
// -----------------------------------
|
|
243
264
|
}
|
|
244
265
|
}
|
|
245
266
|
// if not showing, then show it once it reaches warmup number of frames
|
|
@@ -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++) {
|
|
@@ -192,60 +192,69 @@ class SimpleAR {
|
|
|
192
192
|
const needsRotation = isPortrait && isVideoLandscape;
|
|
193
193
|
// 3. Get intrinsic projection from controller
|
|
194
194
|
const proj = this.controller.projectionTransform;
|
|
195
|
-
//
|
|
196
|
-
// We convert the OpenGL World Matrix to a CSS matrix3d.
|
|
197
|
-
// The OpenGL matrix is column-major. CSS matrix3d is also column-major.
|
|
198
|
-
const m = this.controller.getWorldMatrix(mVT, targetIndex);
|
|
199
|
-
// Map OpenGL coords to Screen Pixels using the projection logic
|
|
200
|
-
const vW = needsRotation ? videoH : videoW;
|
|
201
|
-
const vH = needsRotation ? videoW : videoH;
|
|
202
|
-
const perspectiveScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
203
|
-
const displayW = vW * perspectiveScale;
|
|
204
|
-
const displayH = vH * perspectiveScale;
|
|
205
|
-
const offsetX = (containerRect.width - displayW) / 2;
|
|
206
|
-
const offsetY = (containerRect.height - displayH) / 2;
|
|
207
|
-
// Adjust for centered marker and scaleMultiplier
|
|
208
|
-
const s = finalScale; // We still need the base scale factor for the pixel-to-marker mapping
|
|
209
|
-
// However, a cleaner way is to use the world matrix directly and map it.
|
|
210
|
-
// Actually, the simpler way to do 3D in CSS while keeping my projection logic is:
|
|
211
|
-
// Project the 4 corners and find the homography, OR
|
|
212
|
-
// Use the OpenGL matrix directly with a perspective mapping.
|
|
213
|
-
// Let's use the points projection to maintain the "needsRotation" logic compatibility
|
|
214
|
-
const pMid = projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
195
|
+
// 3. Project 4 corners to determine a full 3D perspective (homography)
|
|
215
196
|
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
216
197
|
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
217
198
|
const pLL = projectToScreen(0, markerH, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
233
241
|
this.overlay.style.maxWidth = 'none';
|
|
234
242
|
this.overlay.style.width = `${markerW}px`;
|
|
235
243
|
this.overlay.style.height = `${markerH}px`;
|
|
236
244
|
this.overlay.style.position = 'absolute';
|
|
237
|
-
this.overlay.style.transformOrigin = '0 0';
|
|
245
|
+
this.overlay.style.transformOrigin = '0 0';
|
|
238
246
|
this.overlay.style.left = '0';
|
|
239
247
|
this.overlay.style.top = '0';
|
|
240
248
|
this.overlay.style.display = 'block';
|
|
241
|
-
//
|
|
242
|
-
//
|
|
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.
|
|
243
253
|
this.overlay.style.transform = `
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
${(pLL.sx - pUL.sx) / markerH}, ${(pLL.sy - pUL.sy) / markerH},
|
|
247
|
-
0, 0)
|
|
254
|
+
matrix3d(${matrix.join(',')})
|
|
255
|
+
translate(${markerW / 2}px, ${markerH / 2}px)
|
|
248
256
|
scale(${this.scaleMultiplier})
|
|
257
|
+
translate(${-markerW / 2}px, ${-markerH / 2}px)
|
|
249
258
|
`;
|
|
250
259
|
}
|
|
251
260
|
// Unified projection logic moved to ./utils/projection.js
|
|
@@ -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): {
|
|
@@ -41,4 +33,11 @@ export class Tracker {
|
|
|
41
33
|
* Pure JS implementation of Bilinear Warping
|
|
42
34
|
*/
|
|
43
35
|
_computeProjection(M: any, inputData: any, prebuilt: any): void;
|
|
36
|
+
/**
|
|
37
|
+
* Refines the target data (Living Mind Map) using actual camera feedback
|
|
38
|
+
* @param {number} targetIndex
|
|
39
|
+
* @param {number} octaveIndex
|
|
40
|
+
* @param {number} alpha - Blending factor (e.g. 0.1 for 10% new data)
|
|
41
|
+
*/
|
|
42
|
+
applyLiveFeedback(targetIndex: number, octaveIndex: number, alpha: number): void;
|
|
44
43
|
}
|
|
@@ -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,
|
|
@@ -217,5 +232,25 @@ class Tracker {
|
|
|
217
232
|
}
|
|
218
233
|
}
|
|
219
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Refines the target data (Living Mind Map) using actual camera feedback
|
|
237
|
+
* @param {number} targetIndex
|
|
238
|
+
* @param {number} octaveIndex
|
|
239
|
+
* @param {number} alpha - Blending factor (e.g. 0.1 for 10% new data)
|
|
240
|
+
*/
|
|
241
|
+
applyLiveFeedback(targetIndex, octaveIndex, alpha) {
|
|
242
|
+
const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
|
|
243
|
+
if (!prebuilt || !prebuilt.projectedImage)
|
|
244
|
+
return;
|
|
245
|
+
const markerPixels = prebuilt.data;
|
|
246
|
+
const projectedPixels = prebuilt.projectedImage;
|
|
247
|
+
const count = markerPixels.length;
|
|
248
|
+
// Blend the projected (camera-sourced) pixels into the marker reference data
|
|
249
|
+
// This allows the NCC matching to adapt to real-world lighting and print quality
|
|
250
|
+
for (let i = 0; i < count; i++) {
|
|
251
|
+
// Simple linear blend
|
|
252
|
+
markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * projectedPixels[i];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
220
255
|
}
|
|
221
256
|
export { Tracker };
|
package/package.json
CHANGED
|
@@ -226,7 +226,7 @@ class Controller {
|
|
|
226
226
|
return { targetIndex: matchedTargetIndex, modelViewTransform };
|
|
227
227
|
}
|
|
228
228
|
async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
|
|
229
|
-
const { worldCoords, screenCoords } = this.tracker.track(
|
|
229
|
+
const { worldCoords, screenCoords, debugExtra } = this.tracker.track(
|
|
230
230
|
inputData,
|
|
231
231
|
lastModelViewTransform,
|
|
232
232
|
targetIndex,
|
|
@@ -236,7 +236,11 @@ class Controller {
|
|
|
236
236
|
worldCoords,
|
|
237
237
|
screenCoords,
|
|
238
238
|
});
|
|
239
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
modelViewTransform,
|
|
241
|
+
inliers: worldCoords.length,
|
|
242
|
+
octaveIndex: debugExtra.octaveIndex
|
|
243
|
+
};
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
processVideo(input) {
|
|
@@ -252,6 +256,7 @@ class Controller {
|
|
|
252
256
|
currentModelViewTransform: null,
|
|
253
257
|
trackCount: 0,
|
|
254
258
|
trackMiss: 0,
|
|
259
|
+
stabilityCount: 0, // Nuevo: Contador para Live Adaptation
|
|
255
260
|
filter: new OneEuroFilter({ minCutOff: this.filterMinCF, beta: this.filterBeta }),
|
|
256
261
|
});
|
|
257
262
|
}
|
|
@@ -291,15 +296,30 @@ class Controller {
|
|
|
291
296
|
const trackingState = this.trackingStates[i];
|
|
292
297
|
|
|
293
298
|
if (trackingState.isTracking) {
|
|
294
|
-
let
|
|
299
|
+
let result = await this._trackAndUpdate(
|
|
295
300
|
inputData,
|
|
296
301
|
trackingState.currentModelViewTransform,
|
|
297
302
|
i,
|
|
298
303
|
);
|
|
299
|
-
if (
|
|
304
|
+
if (result === null) {
|
|
300
305
|
trackingState.isTracking = false;
|
|
306
|
+
trackingState.stabilityCount = 0;
|
|
301
307
|
} else {
|
|
302
|
-
trackingState.currentModelViewTransform = modelViewTransform;
|
|
308
|
+
trackingState.currentModelViewTransform = result.modelViewTransform;
|
|
309
|
+
|
|
310
|
+
// --- LIVE MODEL ADAPTATION LOGIC ---
|
|
311
|
+
// Si el tracking es muy sólido (muchos inliers) y estable, refinamos el modelo
|
|
312
|
+
if (result.inliers > 25) {
|
|
313
|
+
trackingState.stabilityCount++;
|
|
314
|
+
if (trackingState.stabilityCount > 20) { // 20 frames de estabilidad absoluta
|
|
315
|
+
this.tracker.applyLiveFeedback(i, result.octaveIndex, 0.1); // 10% de mezcla real
|
|
316
|
+
if (this.debugMode) console.log(`✨ Live Reification: Target ${i} (Octave ${result.octaveIndex}) updated with real-world textures.`);
|
|
317
|
+
trackingState.stabilityCount = 0; // Reset para la siguiente actualización
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
trackingState.stabilityCount = Math.max(0, trackingState.stabilityCount - 1);
|
|
321
|
+
}
|
|
322
|
+
// -----------------------------------
|
|
303
323
|
}
|
|
304
324
|
}
|
|
305
325
|
|
|
@@ -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++) {
|
|
@@ -222,71 +222,70 @@ class SimpleAR {
|
|
|
222
222
|
// 3. Get intrinsic projection from controller
|
|
223
223
|
const proj = this.controller.projectionTransform;
|
|
224
224
|
|
|
225
|
-
//
|
|
226
|
-
// We convert the OpenGL World Matrix to a CSS matrix3d.
|
|
227
|
-
// The OpenGL matrix is column-major. CSS matrix3d is also column-major.
|
|
228
|
-
const m = this.controller.getWorldMatrix(mVT, targetIndex);
|
|
229
|
-
|
|
230
|
-
// Map OpenGL coords to Screen Pixels using the projection logic
|
|
231
|
-
const vW = needsRotation ? videoH : videoW;
|
|
232
|
-
const vH = needsRotation ? videoW : videoH;
|
|
233
|
-
const perspectiveScale = Math.max(containerRect.width / vW, containerRect.height / vH);
|
|
234
|
-
const displayW = vW * perspectiveScale;
|
|
235
|
-
const displayH = vH * perspectiveScale;
|
|
236
|
-
const offsetX = (containerRect.width - displayW) / 2;
|
|
237
|
-
const offsetY = (containerRect.height - displayH) / 2;
|
|
238
|
-
|
|
239
|
-
// Adjust for centered marker and scaleMultiplier
|
|
240
|
-
const s = finalScale; // We still need the base scale factor for the pixel-to-marker mapping
|
|
241
|
-
// However, a cleaner way is to use the world matrix directly and map it.
|
|
242
|
-
|
|
243
|
-
// Actually, the simpler way to do 3D in CSS while keeping my projection logic is:
|
|
244
|
-
// Project the 4 corners and find the homography, OR
|
|
245
|
-
// Use the OpenGL matrix directly with a perspective mapping.
|
|
246
|
-
|
|
247
|
-
// Let's use the points projection to maintain the "needsRotation" logic compatibility
|
|
248
|
-
const pMid = projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
225
|
+
// 3. Project 4 corners to determine a full 3D perspective (homography)
|
|
249
226
|
const pUL = projectToScreen(0, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
250
227
|
const pUR = projectToScreen(markerW, 0, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
|
|
251
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
|
+
};
|
|
252
267
|
|
|
253
|
-
|
|
254
|
-
const dx = pUR.sx - pUL.sx;
|
|
255
|
-
const dy = pUR.sy - pUL.sy;
|
|
256
|
-
const dz = pUR.sx - pLL.sx; // Not really Z but used for slant
|
|
257
|
-
|
|
258
|
-
const angle = Math.atan2(dy, dx);
|
|
259
|
-
const scaleX = Math.sqrt(dx * dx + dy * dy) / markerW;
|
|
260
|
-
const scaleY = Math.sqrt((pLL.sx - pUL.sx) ** 2 + (pLL.sy - pUL.sy) ** 2) / markerH;
|
|
261
|
-
|
|
262
|
-
// For true 3D tilt, we'll use the projection of the axes
|
|
263
|
-
const screenX = pMid.sx;
|
|
264
|
-
const screenY = pMid.sy;
|
|
265
|
-
|
|
266
|
-
// Final Transform applying 3D perspective via matrix3d derived from projected points
|
|
267
|
-
// NOTE: For full 3D we'd need a homography solver, but for "tilt" we can use the
|
|
268
|
-
// original modelViewTransform if we convert it carefully.
|
|
269
|
-
|
|
270
|
-
const openGLWorldMatrix = this.controller.getWorldMatrix(mVT, targetIndex);
|
|
271
|
-
// We need to apply the same scaling and offsets as projectToScreen to the matrix
|
|
268
|
+
const matrix = solveHomography(markerW, markerH, pUL, pUR, pLL, pLR);
|
|
272
269
|
|
|
270
|
+
// Apply styles
|
|
273
271
|
this.overlay.style.maxWidth = 'none';
|
|
274
272
|
this.overlay.style.width = `${markerW}px`;
|
|
275
273
|
this.overlay.style.height = `${markerH}px`;
|
|
276
274
|
this.overlay.style.position = 'absolute';
|
|
277
|
-
this.overlay.style.transformOrigin = '0 0';
|
|
275
|
+
this.overlay.style.transformOrigin = '0 0';
|
|
278
276
|
this.overlay.style.left = '0';
|
|
279
277
|
this.overlay.style.top = '0';
|
|
280
278
|
this.overlay.style.display = 'block';
|
|
281
279
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
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.
|
|
284
284
|
this.overlay.style.transform = `
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
${(pLL.sx - pUL.sx) / markerH}, ${(pLL.sy - pUL.sy) / markerH},
|
|
288
|
-
0, 0)
|
|
285
|
+
matrix3d(${matrix.join(',')})
|
|
286
|
+
translate(${markerW / 2}px, ${markerH / 2}px)
|
|
289
287
|
scale(${this.scaleMultiplier})
|
|
288
|
+
translate(${-markerW / 2}px, ${-markerH / 2}px)
|
|
290
289
|
`;
|
|
291
290
|
}
|
|
292
291
|
|
|
@@ -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,
|
|
@@ -283,6 +300,28 @@ class Tracker {
|
|
|
283
300
|
}
|
|
284
301
|
}
|
|
285
302
|
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Refines the target data (Living Mind Map) using actual camera feedback
|
|
306
|
+
* @param {number} targetIndex
|
|
307
|
+
* @param {number} octaveIndex
|
|
308
|
+
* @param {number} alpha - Blending factor (e.g. 0.1 for 10% new data)
|
|
309
|
+
*/
|
|
310
|
+
applyLiveFeedback(targetIndex, octaveIndex, alpha) {
|
|
311
|
+
const prebuilt = this.prebuiltData[targetIndex][octaveIndex];
|
|
312
|
+
if (!prebuilt || !prebuilt.projectedImage) return;
|
|
313
|
+
|
|
314
|
+
const markerPixels = prebuilt.data;
|
|
315
|
+
const projectedPixels = prebuilt.projectedImage;
|
|
316
|
+
const count = markerPixels.length;
|
|
317
|
+
|
|
318
|
+
// Blend the projected (camera-sourced) pixels into the marker reference data
|
|
319
|
+
// This allows the NCC matching to adapt to real-world lighting and print quality
|
|
320
|
+
for (let i = 0; i < count; i++) {
|
|
321
|
+
// Simple linear blend
|
|
322
|
+
markerPixels[i] = (1 - alpha) * markerPixels[i] + alpha * projectedPixels[i];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
286
325
|
}
|
|
287
326
|
|
|
288
327
|
export { Tracker };
|