@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.
@@ -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<any>;
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 modelViewTransform;
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 modelViewTransform = await this._trackAndUpdate(inputData, trackingState.currentModelViewTransform, i);
238
- if (modelViewTransform === null) {
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 = [item.trackingData[0]].map((td) => {
220
+ const trackingData = item.trackingData.map((td) => {
221
221
  const count = td.points.length;
222
- // Step 1: Packed Coords - Normalize width/height to 16-bit
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
- // 4. Position calculation via matrix3d (Support for 3D tilt/Z-rotation)
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
- // Using these points we can calculate the 3D rotation and perspective
219
- const dx = pUR.sx - pUL.sx;
220
- const dy = pUR.sy - pUL.sy;
221
- const dz = pUR.sx - pLL.sx; // Not really Z but used for slant
222
- const angle = Math.atan2(dy, dx);
223
- const scaleX = Math.sqrt(dx * dx + dy * dy) / markerW;
224
- const scaleY = Math.sqrt((pLL.sx - pUL.sx) ** 2 + (pLL.sy - pUL.sy) ** 2) / markerH;
225
- // For true 3D tilt, we'll use the projection of the axes
226
- const screenX = pMid.sx;
227
- const screenY = pMid.sy;
228
- // Final Transform applying 3D perspective via matrix3d derived from projected points
229
- // NOTE: For full 3D we'd need a homography solver, but for "tilt" we can use the
230
- // original modelViewTransform if we convert it carefully.
231
- const openGLWorldMatrix = this.controller.getWorldMatrix(mVT, targetIndex);
232
- // We need to apply the same scaling and offsets as projectToScreen to the matrix
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'; // Top-left based for simpler matrix mapping
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
- // Approximate 3D tilt using the projected corners to calculate a skew/scale/rotate combo
242
- // This is more robust than a raw matrix3d if the projection isn't a perfect pinhole
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
- translate(${pUL.sx}px, ${pUL.sy}px)
245
- matrix(${(pUR.sx - pUL.sx) / markerW}, ${(pUR.sy - pUL.sy) / markerW},
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
- this.trackingKeyframeList.push(trackingDataList[i][TRACKING_KEYFRAME]);
19
- }
20
- // Prebuild TypedArrays for features and pixels
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
- const prebuilt = this.prebuiltData[targetIndex];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "AR Compiler for Node.js and Browser",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 modelViewTransform;
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 modelViewTransform = await this._trackAndUpdate(
299
+ let result = await this._trackAndUpdate(
295
300
  inputData,
296
301
  trackingState.currentModelViewTransform,
297
302
  i,
298
303
  );
299
- if (modelViewTransform === null) {
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 = [item.trackingData[0]].map((td) => {
263
+ const trackingData = item.trackingData.map((td) => {
264
264
  const count = td.points.length;
265
- // Step 1: Packed Coords - Normalize width/height to 16-bit
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
- // 4. Position calculation via matrix3d (Support for 3D tilt/Z-rotation)
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
- // Using these points we can calculate the 3D rotation and perspective
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'; // Top-left based for simpler matrix mapping
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
- // Approximate 3D tilt using the projected corners to calculate a skew/scale/rotate combo
283
- // This is more robust than a raw matrix3d if the projection isn't a perfect pinhole
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
- translate(${pUL.sx}px, ${pUL.sy}px)
286
- matrix(${(pUR.sx - pUL.sx) / markerW}, ${(pUR.sy - pUL.sy) / markerW},
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
- for (let i = 0; i < trackingDataList.length; i++) {
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
- // Prebuild TypedArrays for features and pixels
33
- this.prebuiltData = [];
34
- for (let i = 0; i < this.trackingKeyframeList.length; i++) {
35
- const keyframe = this.trackingKeyframeList[i];
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
- const prebuilt = this.prebuiltData[targetIndex];
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 };