@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.
- package/README.md +12 -10
- package/dist/compiler/controller.d.ts +11 -0
- package/dist/compiler/controller.js +12 -4
- package/dist/compiler/detector/crop-detector.js +1 -1
- package/dist/compiler/detector/detector-lite.d.ts +1 -0
- package/dist/compiler/detector/detector-lite.js +7 -1
- package/dist/compiler/matching/hamming-distance.js +33 -6
- package/dist/compiler/matching/matching.js +20 -16
- package/dist/compiler/node-worker.js +1 -1
- package/dist/compiler/offline-compiler.d.ts +28 -24
- package/dist/compiler/offline-compiler.js +112 -35
- package/dist/compiler/simple-ar.d.ts +0 -7
- package/dist/compiler/simple-ar.js +7 -37
- package/dist/compiler/tracker/tracker.js +1 -1
- package/dist/compiler/utils/lsh-binarizer.d.ts +18 -0
- package/dist/compiler/utils/lsh-binarizer.js +37 -0
- package/dist/compiler/utils/projection.d.ts +22 -0
- package/dist/compiler/utils/projection.js +51 -0
- package/package.json +1 -1
- package/src/compiler/controller.js +12 -4
- package/src/compiler/detector/crop-detector.js +1 -1
- package/src/compiler/detector/detector-lite.js +7 -1
- package/src/compiler/matching/hamming-distance.js +38 -6
- package/src/compiler/matching/matching.js +21 -18
- package/src/compiler/node-worker.js +1 -1
- package/src/compiler/offline-compiler.js +128 -35
- package/src/compiler/simple-ar.js +8 -41
- package/src/compiler/tracker/tracker.js +1 -1
- package/src/compiler/utils/lsh-binarizer.js +43 -0
- package/src/compiler/utils/projection.js +58 -0
|
@@ -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
|
|
52
|
-
const d = hammingCompute({ v1: cDesc, v1Offset: idx *
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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)
|
|
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 *
|
|
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 *
|
|
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 =
|
|
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
|
|
40
|
+
// Workers only in Node.js (no en browser)
|
|
41
41
|
if (isNode) {
|
|
42
|
-
|
|
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.
|
|
303
|
-
min: this._columnarize(kf.
|
|
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
|
-
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
|
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
|
-
|
|
355
|
-
|
|
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 (
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 *
|
|
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
|
|
161
|
+
// Flatten modelViewTransform for filtering (3x4 matrix = 12 values)
|
|
161
162
|
const flatMVT = [
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 =
|
|
206
|
-
const pRight =
|
|
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 };
|
|
@@ -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
|
+
}
|