@srsergio/taptapp-ar 1.0.32 → 1.0.33

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.
@@ -18,6 +18,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
18
18
  const qlen = querypoints.length;
19
19
  const kmax = keyframe.max;
20
20
  const kmin = keyframe.min;
21
+ const descSize = 2; // Protocol V6: 64-bit LSH (2 x 32-bit)
21
22
 
22
23
  for (let j = 0; j < qlen; j++) {
23
24
  const querypoint = querypoints[j];
@@ -48,8 +49,8 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
48
49
  for (let k = 0; k < keypointIndexes.length; k++) {
49
50
  const idx = keypointIndexes[k];
50
51
 
51
- // Use offsets to avoid subarray allocation
52
- const d = hammingCompute({ v1: cDesc, v1Offset: idx * 84, v2: qDesc });
52
+ // Use offsets based on detected descriptor size
53
+ const d = hammingCompute({ v1: cDesc, v1Offset: idx * descSize, v2: qDesc });
53
54
 
54
55
  if (d < bestD1) {
55
56
  bestD2 = bestD1;
@@ -60,19 +61,18 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
60
61
  }
61
62
  }
62
63
 
63
- if (
64
- bestIndex !== -1 &&
65
- (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < HAMMING_THRESHOLD)
66
- ) {
67
- matches.push({
68
- querypoint,
69
- keypoint: {
70
- x: col.x[bestIndex],
71
- y: col.y[bestIndex],
72
- angle: col.a[bestIndex],
73
- scale: col.s ? col.s[bestIndex] : keyframe.s
74
- }
75
- });
64
+ if (bestIndex !== -1) {
65
+ if (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < HAMMING_THRESHOLD) {
66
+ matches.push({
67
+ querypoint,
68
+ keypoint: {
69
+ x: col.x[bestIndex],
70
+ y: col.y[bestIndex],
71
+ angle: col.a[bestIndex],
72
+ scale: col.s ? col.s[bestIndex] : keyframe.s
73
+ }
74
+ });
75
+ }
76
76
  }
77
77
  }
78
78
 
@@ -94,7 +94,9 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
94
94
  keyframe: { width: keyframe.w, height: keyframe.h },
95
95
  });
96
96
 
97
- if (H === null) return { debugExtra };
97
+ if (H === null) {
98
+ return { debugExtra };
99
+ }
98
100
 
99
101
  const inlierMatches = _findInlierMatches({
100
102
  H,
@@ -141,7 +143,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
141
143
 
142
144
  if (d2 > dThreshold2) continue;
143
145
 
144
- const d = hammingCompute({ v1: cd, v1Offset: k * 84, v2: qDesc });
146
+ const d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
145
147
 
146
148
  if (d < bestD1) {
147
149
  bestD2 = bestD1;
@@ -200,6 +202,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
200
202
  };
201
203
 
202
204
  const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop }) => {
205
+ const descSize = 2;
203
206
  const isLeaf = node[0] === 1;
204
207
  const childrenOrIndices = node[2];
205
208
 
@@ -221,7 +224,7 @@ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop
221
224
 
222
225
  const d = hammingCompute({
223
226
  v1: descriptors,
224
- v1Offset: cIdx * 84,
227
+ v1Offset: cIdx * descSize,
225
228
  v2: qDesc,
226
229
  });
227
230
  distances[i] = d;
@@ -52,7 +52,7 @@ parentPort.on('message', async (msg) => {
52
52
  const keyframes = [];
53
53
  for (let i = 0; i < imageList.length; i++) {
54
54
  const image = imageList[i];
55
- const detector = new DetectorLite(image.width, image.height);
55
+ const detector = new DetectorLite(image.width, image.height, { useLSH: true });
56
56
 
57
57
  // Detectar features usando JS puro (sin TensorFlow)
58
58
  const { featurePoints: ps } = detector.detect(image.data);
@@ -27,7 +27,7 @@ const isNode = typeof process !== "undefined" &&
27
27
  process.versions != null &&
28
28
  process.versions.node != null;
29
29
 
30
- const CURRENT_VERSION = 3; // Protocol v3: High-performance Columnar Binary Format
30
+ const CURRENT_VERSION = 6; // Protocol v6: Moonshot - LSH 64-bit
31
31
 
32
32
  /**
33
33
  * Compilador offline optimizado sin TensorFlow
@@ -37,9 +37,9 @@ export class OfflineCompiler {
37
37
  this.data = null;
38
38
  this.workerPool = null;
39
39
 
40
- // Workers solo en Node.js (no en browser)
40
+ // Workers only in Node.js (no en browser)
41
41
  if (isNode) {
42
- this._initNodeWorkers();
42
+ // Lazy init workers only when needed
43
43
  } else {
44
44
  console.log("🌐 OfflineCompiler: Browser mode (no workers)");
45
45
  }
@@ -68,7 +68,6 @@ export class OfflineCompiler {
68
68
  const numWorkers = Math.min(os.cpus().length, 4);
69
69
 
70
70
  this.workerPool = new WorkerPool(workerPath, numWorkers, Worker);
71
- console.log(`🚀 OfflineCompiler: Node.js mode with ${numWorkers} workers`);
72
71
  } catch (e) {
73
72
  console.log("⚡ OfflineCompiler: Running without workers (initialization failed)", e);
74
73
  }
@@ -142,6 +141,7 @@ export class OfflineCompiler {
142
141
  let currentPercent = 0;
143
142
 
144
143
  // Use workers if available
144
+ if (isNode) await this._initNodeWorkers();
145
145
  if (this.workerPool) {
146
146
  const progressMap = new Float32Array(targetImages.length);
147
147
 
@@ -172,7 +172,7 @@ export class OfflineCompiler {
172
172
  const keyframes = [];
173
173
 
174
174
  for (const image of imageList) {
175
- const detector = new DetectorLite(image.width, image.height);
175
+ const detector = new DetectorLite(image.width, image.height, { useLSH: true });
176
176
  const { featurePoints: ps } = detector.detect(image.data);
177
177
 
178
178
  const maximaPoints = ps.filter((p) => p.maxima);
@@ -260,8 +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.map((td) => {
263
+ const trackingData = [item.trackingData[0]].map((td) => {
264
264
  const count = td.points.length;
265
+ // Step 1: Packed Coords - Normalize width/height to 16-bit
265
266
  const px = new Float32Array(count);
266
267
  const py = new Float32Array(count);
267
268
  for (let i = 0; i < count; i++) {
@@ -294,30 +295,71 @@ export class OfflineCompiler {
294
295
  });
295
296
  }
296
297
 
298
+ _getMorton(x, y) {
299
+ // Interleave bits of x and y
300
+ let x_int = x | 0;
301
+ let y_int = y | 0;
302
+
303
+ x_int = (x_int | (x_int << 8)) & 0x00FF00FF;
304
+ x_int = (x_int | (x_int << 4)) & 0x0F0F0F0F;
305
+ x_int = (x_int | (x_int << 2)) & 0x33333333;
306
+ x_int = (x_int | (x_int << 1)) & 0x55555555;
307
+
308
+ y_int = (y_int | (y_int << 8)) & 0x00FF00FF;
309
+ y_int = (y_int | (y_int << 4)) & 0x0F0F0F0F;
310
+ y_int = (y_int | (y_int << 2)) & 0x33333333;
311
+ y_int = (y_int | (y_int << 1)) & 0x55555555;
312
+
313
+ return x_int | (y_int << 1);
314
+ }
315
+
297
316
  _packKeyframe(kf) {
317
+ // Step 2.1: Morton Sorting - Sort points spatially to improve Delta-Descriptor XOR
318
+ const sortPoints = (points) => {
319
+ return [...points].sort((a, b) => {
320
+ return this._getMorton(a.x, a.y) - this._getMorton(b.x, b.y);
321
+ });
322
+ };
323
+
324
+ const sortedMaxima = sortPoints(kf.maximaPoints);
325
+ const sortedMinima = sortPoints(kf.minimaPoints);
326
+
327
+ // Rebuild clusters with sorted indices
328
+ const sortedMaximaCluster = hierarchicalClusteringBuild({ points: sortedMaxima });
329
+ const sortedMinimaCluster = hierarchicalClusteringBuild({ points: sortedMinima });
330
+
298
331
  return {
299
332
  w: kf.width,
300
333
  h: kf.height,
301
334
  s: kf.scale,
302
- max: this._columnarize(kf.maximaPoints, kf.maximaPointsCluster),
303
- min: this._columnarize(kf.minimaPoints, kf.minimaPointsCluster),
335
+ max: this._columnarize(sortedMaxima, sortedMaximaCluster, kf.width, kf.height),
336
+ min: this._columnarize(sortedMinima, sortedMinimaCluster, kf.width, kf.height),
304
337
  };
305
338
  }
306
339
 
307
- _columnarize(points, tree) {
340
+ _columnarize(points, tree, width, height) {
308
341
  const count = points.length;
309
- const x = new Float32Array(count);
310
- const y = new Float32Array(count);
311
- const angle = new Float32Array(count);
312
- const scale = new Float32Array(count);
313
- const descriptors = new Uint8Array(count * 84); // 84 bytes per point (FREAK)
342
+ // Step 1: Packed Coords - Normalize to 16-bit
343
+ const x = new Uint16Array(count);
344
+ const y = new Uint16Array(count);
345
+ // Step 1.1: Angle Quantization - Int16
346
+ const angle = new Int16Array(count);
347
+ // Step 1.2: Scale Indexing - Uint8
348
+ const scale = new Uint8Array(count);
349
+
350
+ // Step 3: LSH 64-bit Descriptors - Uint32Array (2 elements per point)
351
+ const descriptors = new Uint32Array(count * 2);
314
352
 
315
353
  for (let i = 0; i < count; i++) {
316
- x[i] = points[i].x;
317
- y[i] = points[i].y;
318
- angle[i] = points[i].angle;
319
- scale[i] = points[i].scale;
320
- descriptors.set(points[i].descriptors, i * 84);
354
+ x[i] = Math.round((points[i].x / width) * 65535);
355
+ y[i] = Math.round((points[i].y / height) * 65535);
356
+ angle[i] = Math.round((points[i].angle / Math.PI) * 32767);
357
+ scale[i] = Math.round(Math.log2(points[i].scale || 1));
358
+
359
+ if (points[i].descriptors && points[i].descriptors.length >= 2) {
360
+ descriptors[i * 2] = points[i].descriptors[0];
361
+ descriptors[(i * 2) + 1] = points[i].descriptors[1];
362
+ }
321
363
  }
322
364
 
323
365
  return {
@@ -340,35 +382,84 @@ export class OfflineCompiler {
340
382
  importData(buffer) {
341
383
  const content = msgpack.decode(new Uint8Array(buffer));
342
384
 
343
- if (!content.v || content.v !== CURRENT_VERSION) {
344
- console.error("Incompatible .mind version. Required: " + CURRENT_VERSION);
385
+ const version = content.v || 0;
386
+ if (version !== CURRENT_VERSION && version !== 5) {
387
+ console.error(`Incompatible .mind version: ${version}. This engine only supports Protocol V5/V6.`);
345
388
  return [];
346
389
  }
390
+ const descSize = version >= 6 ? 2 : 4;
347
391
 
348
- // Restore Float32Arrays from Uint8Arrays returned by msgpack
392
+ // Restore TypedArrays from Uint8Arrays returned by msgpack
349
393
  const dataList = content.dataList;
350
394
  for (let i = 0; i < dataList.length; i++) {
351
395
  const item = dataList[i];
396
+
397
+ // Unpack Tracking Data
398
+ for (const td of item.trackingData) {
399
+ let px = td.px;
400
+ let py = td.py;
401
+
402
+ if (px instanceof Uint8Array) {
403
+ px = new Float32Array(px.buffer.slice(px.byteOffset, px.byteOffset + px.byteLength));
404
+ }
405
+ if (py instanceof Uint8Array) {
406
+ py = new Float32Array(py.buffer.slice(py.byteOffset, py.byteOffset + py.byteLength));
407
+ }
408
+ td.px = px;
409
+ td.py = py;
410
+ }
411
+
412
+ // Unpack Matching Data
352
413
  for (const kf of item.matchingData) {
353
414
  for (const col of [kf.max, kf.min]) {
354
- if (col.x instanceof Uint8Array) {
355
- col.x = new Float32Array(col.x.buffer.slice(col.x.byteOffset, col.x.byteOffset + col.x.byteLength));
415
+ let xRaw = col.x;
416
+ let yRaw = col.y;
417
+
418
+ if (xRaw instanceof Uint8Array) {
419
+ xRaw = new Uint16Array(xRaw.buffer.slice(xRaw.byteOffset, xRaw.byteOffset + xRaw.byteLength));
356
420
  }
357
- if (col.y instanceof Uint8Array) {
358
- col.y = new Float32Array(col.y.buffer.slice(col.y.byteOffset, col.y.byteOffset + col.y.byteLength));
421
+ if (yRaw instanceof Uint8Array) {
422
+ yRaw = new Uint16Array(yRaw.buffer.slice(yRaw.byteOffset, yRaw.byteOffset + yRaw.byteLength));
359
423
  }
424
+
425
+ // Rescale for compatibility with Matcher
426
+ const count = xRaw.length;
427
+ const x = new Float32Array(count);
428
+ const y = new Float32Array(count);
429
+ for (let k = 0; k < count; k++) {
430
+ x[k] = (xRaw[k] / 65535) * kf.w;
431
+ y[k] = (yRaw[k] / 65535) * kf.h;
432
+ }
433
+ col.x = x;
434
+ col.y = y;
435
+
360
436
  if (col.a instanceof Uint8Array) {
361
- col.a = new Float32Array(col.a.buffer.slice(col.a.byteOffset, col.a.byteOffset + col.a.byteLength));
437
+ const aRaw = new Int16Array(col.a.buffer.slice(col.a.byteOffset, col.a.byteOffset + col.a.byteLength));
438
+ const a = new Float32Array(count);
439
+ for (let k = 0; k < count; k++) {
440
+ a[k] = (aRaw[k] / 32767) * Math.PI;
441
+ }
442
+ col.a = a;
362
443
  }
363
444
  if (col.s instanceof Uint8Array) {
364
- col.s = new Float32Array(col.s.buffer.slice(col.s.byteOffset, col.s.byteOffset + col.s.byteLength));
445
+ const sRaw = col.s;
446
+ const s = new Float32Array(count);
447
+ for (let k = 0; k < count; k++) {
448
+ s[k] = Math.pow(2, sRaw[k]);
449
+ }
450
+ col.s = s;
451
+ }
452
+
453
+ // Restore LSH descriptors (Uint32Array)
454
+ if (col.d instanceof Uint8Array) {
455
+ col.d = new Uint32Array(col.d.buffer.slice(col.d.byteOffset, col.d.byteOffset + col.d.byteLength));
365
456
  }
366
457
  }
367
458
  }
368
459
  }
369
460
 
370
461
  this.data = dataList;
371
- return this.data;
462
+ return { version, dataList };
372
463
  }
373
464
 
374
465
  _unpackKeyframe(kf) {
@@ -376,23 +467,25 @@ export class OfflineCompiler {
376
467
  width: kf.w,
377
468
  height: kf.h,
378
469
  scale: kf.s,
379
- maximaPoints: this._decolumnarize(kf.max),
380
- minimaPoints: this._decolumnarize(kf.min),
470
+ maximaPoints: this._decolumnarize(kf.max, kf.w, kf.h),
471
+ minimaPoints: this._decolumnarize(kf.min, kf.w, kf.h),
381
472
  maximaPointsCluster: { rootNode: this._expandTree(kf.max.t) },
382
473
  minimaPointsCluster: { rootNode: this._expandTree(kf.min.t) },
383
474
  };
384
475
  }
385
476
 
386
- _decolumnarize(col) {
477
+ _decolumnarize(col, width, height) {
387
478
  const points = [];
388
479
  const count = col.x.length;
480
+ const descSize = col.d.length / count;
481
+
389
482
  for (let i = 0; i < count; i++) {
390
483
  points.push({
391
- x: col.x[i],
392
- y: col.y[i],
484
+ x: (col.x[i] / 65535) * width,
485
+ y: (col.y[i] / 65535) * height,
393
486
  angle: col.a[i],
394
487
  scale: col.s ? col.s[i] : 1.0,
395
- descriptors: col.d.slice(i * 84, (i + 1) * 84),
488
+ descriptors: col.d.slice(i * descSize, (i + 1) * descSize),
396
489
  });
397
490
  }
398
491
  return points;
@@ -1,5 +1,6 @@
1
1
  import { Controller } from "./controller.js";
2
2
  import { OneEuroFilter } from "../libs/one-euro-filter.js";
3
+ import { projectToScreen } from "./utils/projection.js";
3
4
 
4
5
  /**
5
6
  * 🍦 SimpleAR - Dead-simple vanilla AR for image overlays
@@ -157,11 +158,11 @@ class SimpleAR {
157
158
  this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.1, beta: 0.01 });
158
159
  }
159
160
 
160
- // Flatten mVT for filtering (3x4 matrix = 12 values)
161
+ // Flatten modelViewTransform for filtering (3x4 matrix = 12 values)
161
162
  const flatMVT = [
162
- mVT[0][0], mVT[0][1], mVT[0][2], mVT[0][3],
163
- mVT[1][0], mVT[1][1], mVT[1][2], mVT[1][3],
164
- mVT[2][0], mVT[2][1], mVT[2][2], mVT[2][3]
163
+ modelViewTransform[0][0], modelViewTransform[0][1], modelViewTransform[0][2], modelViewTransform[0][3],
164
+ modelViewTransform[1][0], modelViewTransform[1][1], modelViewTransform[1][2], modelViewTransform[1][3],
165
+ modelViewTransform[2][0], modelViewTransform[2][1], modelViewTransform[2][2], modelViewTransform[2][3]
165
166
  ];
166
167
  const smoothedFlat = this.filters[targetIndex].filter(Date.now(), flatMVT);
167
168
  const smoothedMVT = [
@@ -202,8 +203,8 @@ class SimpleAR {
202
203
 
203
204
  // 3. Project 3 points to determine position, scale, and rotation
204
205
  // Points in Marker Space: Center, Right-Edge, and Down-Edge
205
- const pMid = this._projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
206
- const pRight = this._projectToScreen(markerW / 2 + 100, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
206
+ const pMid = projectToScreen(markerW / 2, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
207
+ const pRight = projectToScreen(markerW / 2 + 100, markerH / 2, 0, mVT, proj, videoW, videoH, containerRect, needsRotation);
207
208
 
208
209
  // 4. Calculate Screen Position
209
210
  const screenX = pMid.sx;
@@ -252,41 +253,7 @@ class SimpleAR {
252
253
  `;
253
254
  }
254
255
 
255
- /**
256
- * Projects a 3D marker-space point all the way to 2D screen CSS pixels
257
- */
258
- _projectToScreen(x, y, z, mVT, proj, videoW, videoH, containerRect, needsRotation) {
259
- // Marker -> Camera Space
260
- const tx = mVT[0][0] * x + mVT[0][1] * y + mVT[0][2] * z + mVT[0][3];
261
- const ty = mVT[1][0] * x + mVT[1][1] * y + mVT[1][2] * z + mVT[1][3];
262
- const tz = mVT[2][0] * x + mVT[2][1] * y + mVT[2][2] * z + mVT[2][3];
263
-
264
- // Camera -> Buffer Pixels (e.g. 1280x720)
265
- const bx = (proj[0][0] * tx / tz) + proj[0][2];
266
- const by = (proj[1][1] * ty / tz) + proj[1][2];
267
-
268
- // Buffer -> Screen CSS Pixels
269
- const vW = needsRotation ? videoH : videoW;
270
- const vH = needsRotation ? videoW : videoH;
271
- const perspectiveScale = Math.max(containerRect.width / vW, containerRect.height / vH);
272
-
273
- const displayW = vW * perspectiveScale;
274
- const displayH = vH * perspectiveScale;
275
- const offsetX = (containerRect.width - displayW) / 2;
276
- const offsetY = (containerRect.height - displayH) / 2;
277
-
278
- let sx, sy;
279
- if (needsRotation) {
280
- // Mapping: Camera +X (Right) -> Screen +Y (Down), Camera +Y (Down) -> Screen -X (Left)
281
- sx = offsetX + (displayW / 2) - (by - proj[1][2]) * perspectiveScale;
282
- sy = offsetY + (displayH / 2) + (bx - proj[0][2]) * perspectiveScale;
283
- } else {
284
- sx = offsetX + (displayW / 2) + (bx - proj[0][2]) * perspectiveScale;
285
- sy = offsetY + (displayH / 2) + (by - proj[1][2]) * perspectiveScale;
286
- }
287
-
288
- return { sx, sy };
289
- }
256
+ // Unified projection logic moved to ./utils/projection.js
290
257
  }
291
258
 
292
259
  export { SimpleAR };
@@ -4,7 +4,7 @@ const AR2_DEFAULT_TS = 6;
4
4
  const AR2_DEFAULT_TS_GAP = 1;
5
5
  const AR2_SEARCH_SIZE = 10;
6
6
  const AR2_SEARCH_GAP = 1;
7
- const AR2_SIM_THRESH = 0.8;
7
+ const AR2_SIM_THRESH = 0.6;
8
8
 
9
9
  const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
10
10
 
@@ -0,0 +1,43 @@
1
+ /**
2
+ * LSH Binarizer for FREAK descriptors.
3
+ *
4
+ * This utility implements Locality Sensitive Hashing (LSH) for binary descriptors.
5
+ * It uses simple Bit-Sampling to reduce the 672 bits (84 bytes) of a FREAK
6
+ * descriptor into a 64-bit (8-byte) fingerprint.
7
+ *
8
+ * Bit-sampling is chosen for maximum speed and zero memory overhead,
9
+ * which fits the Moonshot goal of an ultra-lightweight bundle.
10
+ */
11
+
12
+ // For 64-bit LSH, we use a uniform sampling across the 672-bit descriptor.
13
+ const SAMPLING_INDICES = new Int32Array(64);
14
+ for (let i = 0; i < 64; i++) {
15
+ SAMPLING_INDICES[i] = Math.floor(i * (672 / 64));
16
+ }
17
+
18
+ /**
19
+ * Converts an 84-byte FREAK descriptor into a Uint32Array of 2 elements (64 bits).
20
+ * @param {Uint8Array} descriptor - The 84-byte FREAK descriptor.
21
+ * @returns {Uint32Array} Array of two 32-bit integers.
22
+ */
23
+ export function binarizeFREAK64(descriptor) {
24
+ const result = new Uint32Array(2);
25
+
26
+ for (let i = 0; i < 64; i++) {
27
+ const bitIndex = SAMPLING_INDICES[i];
28
+ const byteIdx = bitIndex >> 3;
29
+ const bitIdx = 7 - (bitIndex & 7);
30
+
31
+ if ((descriptor[byteIdx] >> bitIdx) & 1) {
32
+ const uintIdx = i >> 5; // i / 32
33
+ const uintBitIdx = i & 31; // i % 32
34
+ result[uintIdx] |= (1 << uintBitIdx);
35
+ }
36
+ }
37
+
38
+ return result;
39
+ }
40
+
41
+ // Backward compatibility or for other uses
42
+ export const binarizeFREAK128 = binarizeFREAK64;
43
+ export const binarizeFREAK32 = binarizeFREAK64;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 📐 AR Projection Utilities
3
+ * Common logic for projecting 3D marker-space points to 2D screen CSS pixels.
4
+ */
5
+
6
+ /**
7
+ * Projects a 3D marker-space point (x, y, z) into 2D screen coordinates.
8
+ *
9
+ * @param {number} x - Marker X coordinate
10
+ * @param {number} y - Marker Y coordinate
11
+ * @param {number} z - Marker Z coordinate (height from surface)
12
+ * @param {number[][]} mVT - ModelViewTransform matrix (3x4)
13
+ * @param {number[][]} proj - Projection matrix (3x3)
14
+ * @param {number} videoW - Internal video width
15
+ * @param {number} videoH - Internal video height
16
+ * @param {Object} containerRect - {width, height} of the display container
17
+ * @param {boolean} needsRotation - Whether the feed needs 90deg rotation (e.g. portrait mobile)
18
+ * @returns {{sx: number, sy: number}} Screen coordinates [X, Y]
19
+ */
20
+ export function projectToScreen(x, y, z, mVT, proj, videoW, videoH, containerRect, needsRotation = false) {
21
+ // 1. Marker Space -> Camera Space (3D)
22
+ const tx = mVT[0][0] * x + mVT[0][1] * y + mVT[0][2] * z + mVT[0][3];
23
+ const ty = mVT[1][0] * x + mVT[1][1] * y + mVT[1][2] * z + mVT[1][3];
24
+ const tz = mVT[2][0] * x + mVT[2][1] * y + mVT[2][2] * z + mVT[2][3];
25
+
26
+ // 2. Camera Space -> Buffer Pixels (2D)
27
+ // Using intrinsic projection from controller
28
+ const bx = (proj[0][0] * tx / tz) + proj[0][2];
29
+ const by = (proj[1][1] * ty / tz) + proj[1][2];
30
+
31
+ // 3. Buffer Pixels -> Screen CSS Pixels
32
+ const vW = needsRotation ? videoH : videoW;
33
+ const vH = needsRotation ? videoW : videoH;
34
+
35
+ // Calculate how the video is scaled to cover (object-fit: cover) the container
36
+ const perspectiveScale = Math.max(containerRect.width / vW, containerRect.height / vH);
37
+
38
+ const displayW = vW * perspectiveScale;
39
+ const displayH = vH * perspectiveScale;
40
+
41
+ // Centering offsets
42
+ const offsetX = (containerRect.width - displayW) / 2;
43
+ const offsetY = (containerRect.height - displayH) / 2;
44
+
45
+ let sx, sy;
46
+ if (needsRotation) {
47
+ // Rotation Mapping:
48
+ // Camera +X (Right) -> Screen +Y (Down)
49
+ // Camera +Y (Down) -> Screen -X (Left)
50
+ sx = offsetX + (displayW / 2) - (by - proj[1][2]) * perspectiveScale;
51
+ sy = offsetY + (displayH / 2) + (bx - proj[0][2]) * perspectiveScale;
52
+ } else {
53
+ sx = offsetX + (displayW / 2) + (bx - proj[0][2]) * perspectiveScale;
54
+ sy = offsetY + (displayH / 2) + (by - proj[1][2]) * perspectiveScale;
55
+ }
56
+
57
+ return { sx, sy };
58
+ }