@srsergio/taptapp-ar 1.0.88 → 1.0.90
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 +17 -10
- package/dist/compiler/offline-compiler.js +16 -4
- package/dist/core/detector/detector-lite.d.ts +1 -0
- package/dist/core/detector/detector-lite.js +31 -15
- package/dist/core/estimation/estimate.d.ts +7 -0
- package/dist/core/estimation/estimate.js +13 -48
- package/dist/core/estimation/morph-refinement.d.ts +8 -0
- package/dist/core/estimation/morph-refinement.js +116 -0
- package/dist/core/estimation/pnp-solver.d.ts +5 -0
- package/dist/core/estimation/pnp-solver.js +109 -0
- package/dist/core/input-loader.js +19 -2
- package/dist/core/matching/hdc.d.ts +27 -0
- package/dist/core/matching/hdc.js +102 -0
- package/dist/core/matching/hierarchical-clustering.d.ts +1 -3
- package/dist/core/matching/hierarchical-clustering.js +30 -29
- package/dist/core/matching/hough.js +12 -11
- package/dist/core/matching/matcher.d.ts +4 -0
- package/dist/core/matching/matcher.js +23 -8
- package/dist/core/matching/matching.d.ts +22 -2
- package/dist/core/matching/matching.js +169 -39
- package/dist/core/matching/ransacHomography.js +3 -6
- package/dist/core/protocol.d.ts +5 -3
- package/dist/core/protocol.js +28 -6
- package/dist/runtime/controller.js +19 -14
- package/dist/runtime/controller.worker.js +4 -1
- package/package.json +3 -2
- package/src/compiler/offline-compiler.ts +17 -4
- package/src/core/detector/detector-lite.js +32 -15
- package/src/core/estimation/estimate.js +14 -63
- package/src/core/estimation/morph-refinement.js +139 -0
- package/src/core/estimation/pnp-solver.js +131 -0
- package/src/core/input-loader.js +21 -2
- package/src/core/matching/hdc.ts +117 -0
- package/src/core/matching/hierarchical-clustering.js +30 -29
- package/src/core/matching/hough.js +12 -11
- package/src/core/matching/matcher.js +27 -9
- package/src/core/matching/matching.js +192 -39
- package/src/core/matching/ransacHomography.js +3 -6
- package/src/core/protocol.ts +26 -6
- package/src/runtime/controller.ts +20 -14
- package/src/runtime/controller.worker.js +4 -1
|
@@ -3,18 +3,28 @@ import { compute as hammingCompute } from "./hamming-distance.js";
|
|
|
3
3
|
import { computeHoughMatches } from "./hough.js";
|
|
4
4
|
import { computeHomography } from "./ransacHomography.js";
|
|
5
5
|
import { multiplyPointHomographyInhomogenous, matrixInverse33 } from "../utils/geometry.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
6
|
+
import { refineWithMorphology } from "../estimation/morph-refinement.js";
|
|
7
|
+
import { popcount32 } from "./hierarchical-clustering.js";
|
|
8
|
+
const INLIER_THRESHOLD = 15.0; // 🚀 More tolerance for initial noise
|
|
9
|
+
const MIN_NUM_INLIERS = 6; // 🎯 Lowered from 8 to catch the marker earlier
|
|
10
|
+
const CLUSTER_MAX_POP = 25;
|
|
11
|
+
const HAMMING_THRESHOLD = 0.85;
|
|
12
|
+
const HDC_RATIO_THRESHOLD = 0.85;
|
|
13
|
+
const MAX_MATCH_QUERY_POINTS = 800; // 🚀 Increased to handle 3000+ points detected
|
|
10
14
|
// match list of querpoints against pre-built list of keyframes
|
|
11
|
-
const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) => {
|
|
15
|
+
const match = ({ keyframe, querypoints: rawQuerypoints, querywidth, queryheight, debugMode }) => {
|
|
12
16
|
let debugExtra = {};
|
|
17
|
+
// 🎯 Performance Optimizer: Use only the most "salient" points (highest response)
|
|
18
|
+
const querypoints = rawQuerypoints.length > MAX_MATCH_QUERY_POINTS
|
|
19
|
+
? [...rawQuerypoints].sort((a, b) => (b.score || b.response || 0) - (a.score || a.response || 0)).slice(0, MAX_MATCH_QUERY_POINTS)
|
|
20
|
+
: rawQuerypoints;
|
|
13
21
|
const matches = [];
|
|
14
22
|
const qlen = querypoints.length;
|
|
15
23
|
const kmax = keyframe.max;
|
|
16
24
|
const kmin = keyframe.min;
|
|
17
|
-
const
|
|
25
|
+
const isHDC = keyframe.hdc === true || (kmax && kmax.hdc === 1);
|
|
26
|
+
const descSize = isHDC ? 1 : 2;
|
|
27
|
+
const currentRatioThreshold = isHDC ? HDC_RATIO_THRESHOLD : HAMMING_THRESHOLD;
|
|
18
28
|
for (let j = 0; j < qlen; j++) {
|
|
19
29
|
const querypoint = querypoints[j];
|
|
20
30
|
const col = querypoint.maxima ? kmax : kmin;
|
|
@@ -23,14 +33,15 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
23
33
|
const rootNode = col.t;
|
|
24
34
|
const keypointIndexes = [];
|
|
25
35
|
const queue = new TinyQueue([], (a1, a2) => a1.d - a2.d);
|
|
26
|
-
// query potential candidates from the columnar tree
|
|
27
36
|
_query({
|
|
28
37
|
node: rootNode,
|
|
29
38
|
descriptors: col.d,
|
|
30
39
|
querypoint,
|
|
31
40
|
queue,
|
|
32
41
|
keypointIndexes,
|
|
33
|
-
numPop: 0
|
|
42
|
+
numPop: 0,
|
|
43
|
+
isHDC,
|
|
44
|
+
descSize
|
|
34
45
|
});
|
|
35
46
|
let bestIndex = -1;
|
|
36
47
|
let bestD1 = Number.MAX_SAFE_INTEGER;
|
|
@@ -39,8 +50,13 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
39
50
|
const cDesc = col.d;
|
|
40
51
|
for (let k = 0; k < keypointIndexes.length; k++) {
|
|
41
52
|
const idx = keypointIndexes[k];
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
let d;
|
|
54
|
+
if (isHDC) {
|
|
55
|
+
d = popcount32(cDesc[idx] ^ querypoint.hdcSignature);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
d = hammingCompute({ v1: cDesc, v1Offset: idx * descSize, v2: qDesc });
|
|
59
|
+
}
|
|
44
60
|
if (d < bestD1) {
|
|
45
61
|
bestD2 = bestD1;
|
|
46
62
|
bestD1 = d;
|
|
@@ -51,7 +67,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
51
67
|
}
|
|
52
68
|
}
|
|
53
69
|
if (bestIndex !== -1) {
|
|
54
|
-
if (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) <
|
|
70
|
+
if (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < currentRatioThreshold) {
|
|
55
71
|
matches.push({
|
|
56
72
|
querypoint,
|
|
57
73
|
keypoint: {
|
|
@@ -59,26 +75,39 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
59
75
|
y: col.y[bestIndex],
|
|
60
76
|
angle: col.a[bestIndex],
|
|
61
77
|
scale: col.s ? col.s[bestIndex] : keyframe.s
|
|
62
|
-
}
|
|
78
|
+
},
|
|
79
|
+
d: bestD1
|
|
63
80
|
});
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
|
-
if (matches.length < MIN_NUM_INLIERS)
|
|
84
|
+
if (matches.length < MIN_NUM_INLIERS) {
|
|
68
85
|
return { debugExtra };
|
|
86
|
+
}
|
|
87
|
+
// Debug: Log Hamming results
|
|
88
|
+
if (Math.random() < 0.1 && debugMode) {
|
|
89
|
+
console.log(`MATCH_DL: Hamming found ${matches.length} initial matches`);
|
|
90
|
+
}
|
|
91
|
+
// 🌌 Moonshot: Constellation matching disabled for performance calibration
|
|
92
|
+
const constellationMatches = matches;
|
|
93
|
+
if (debugMode)
|
|
94
|
+
debugExtra.constellationMatches = constellationMatches;
|
|
69
95
|
const houghMatches = computeHoughMatches({
|
|
70
|
-
keywidth: keyframe.w,
|
|
71
|
-
keyheight: keyframe.h,
|
|
96
|
+
keywidth: keyframe.w || keyframe.width,
|
|
97
|
+
keyheight: keyframe.h || keyframe.height,
|
|
72
98
|
querywidth,
|
|
73
99
|
queryheight,
|
|
74
|
-
matches,
|
|
100
|
+
matches: constellationMatches,
|
|
75
101
|
});
|
|
76
102
|
if (debugMode)
|
|
77
103
|
debugExtra.houghMatches = houghMatches;
|
|
104
|
+
if (houghMatches.length < MIN_NUM_INLIERS) {
|
|
105
|
+
return { debugExtra };
|
|
106
|
+
}
|
|
78
107
|
const H = computeHomography({
|
|
79
108
|
srcPoints: houghMatches.map((m) => [m.keypoint.x, m.keypoint.y]),
|
|
80
109
|
dstPoints: houghMatches.map((m) => [m.querypoint.x, m.querypoint.y]),
|
|
81
|
-
keyframe: { width: keyframe.w, height: keyframe.h },
|
|
110
|
+
keyframe: { width: keyframe.w || keyframe.width, height: keyframe.h || keyframe.height },
|
|
82
111
|
});
|
|
83
112
|
if (H === null) {
|
|
84
113
|
return { debugExtra };
|
|
@@ -90,11 +119,14 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
90
119
|
});
|
|
91
120
|
if (debugMode)
|
|
92
121
|
debugExtra.inlierMatches = inlierMatches;
|
|
93
|
-
if (inlierMatches.length < MIN_NUM_INLIERS)
|
|
122
|
+
if (inlierMatches.length < MIN_NUM_INLIERS) {
|
|
94
123
|
return { debugExtra };
|
|
95
|
-
|
|
124
|
+
}
|
|
125
|
+
if (debugMode && Math.random() < 0.02) {
|
|
126
|
+
console.log(`MATCH: Homography success with ${inlierMatches.length} inliers`);
|
|
127
|
+
}
|
|
96
128
|
const HInv = matrixInverse33(H, 0.00001);
|
|
97
|
-
const dThreshold2 = 100;
|
|
129
|
+
const dThreshold2 = 100;
|
|
98
130
|
const matches2 = [];
|
|
99
131
|
const hi00 = HInv[0], hi01 = HInv[1], hi02 = HInv[2];
|
|
100
132
|
const hi10 = HInv[3], hi11 = HInv[4], hi12 = HInv[5];
|
|
@@ -102,7 +134,6 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
102
134
|
for (let j = 0; j < qlen; j++) {
|
|
103
135
|
const querypoint = querypoints[j];
|
|
104
136
|
const qx = querypoint.x, qy = querypoint.y;
|
|
105
|
-
// Inline multiplyPointHomographyInhomogenous
|
|
106
137
|
const uz = (qx * hi20) + (qy * hi21) + hi22;
|
|
107
138
|
const invZ = 1.0 / uz;
|
|
108
139
|
const mapX = ((qx * hi00) + (qy * hi01) + hi02) * invZ;
|
|
@@ -121,7 +152,13 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
121
152
|
const d2 = dx * dx + dy * dy;
|
|
122
153
|
if (d2 > dThreshold2)
|
|
123
154
|
continue;
|
|
124
|
-
|
|
155
|
+
let d;
|
|
156
|
+
if (isHDC) {
|
|
157
|
+
d = popcount32(cd[k] ^ querypoint.hdcSignature);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
|
|
161
|
+
}
|
|
125
162
|
if (d < bestD1) {
|
|
126
163
|
bestD2 = bestD1;
|
|
127
164
|
bestD1 = d;
|
|
@@ -132,7 +169,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
132
169
|
}
|
|
133
170
|
}
|
|
134
171
|
if (bestIndex !== -1 &&
|
|
135
|
-
(bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) <
|
|
172
|
+
(bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < currentRatioThreshold)) {
|
|
136
173
|
matches2.push({
|
|
137
174
|
querypoint,
|
|
138
175
|
keypoint: {
|
|
@@ -147,8 +184,8 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
147
184
|
if (debugMode)
|
|
148
185
|
debugExtra.matches2 = matches2;
|
|
149
186
|
const houghMatches2 = computeHoughMatches({
|
|
150
|
-
keywidth: keyframe.w,
|
|
151
|
-
keyheight: keyframe.h,
|
|
187
|
+
keywidth: keyframe.w || keyframe.width,
|
|
188
|
+
keyheight: keyframe.h || keyframe.height,
|
|
152
189
|
querywidth,
|
|
153
190
|
queryheight,
|
|
154
191
|
matches: matches2,
|
|
@@ -158,7 +195,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
158
195
|
const H2 = computeHomography({
|
|
159
196
|
srcPoints: houghMatches2.map((m) => [m.keypoint.x, m.keypoint.y]),
|
|
160
197
|
dstPoints: houghMatches2.map((m) => [m.querypoint.x, m.querypoint.y]),
|
|
161
|
-
keyframe: { width: keyframe.w, height: keyframe.h },
|
|
198
|
+
keyframe: { width: keyframe.w || keyframe.width, height: keyframe.h || keyframe.height },
|
|
162
199
|
});
|
|
163
200
|
if (H2 === null)
|
|
164
201
|
return { debugExtra };
|
|
@@ -169,10 +206,17 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
|
|
|
169
206
|
});
|
|
170
207
|
if (debugMode)
|
|
171
208
|
debugExtra.inlierMatches2 = inlierMatches2;
|
|
172
|
-
|
|
209
|
+
const refinedH = refineWithMorphology({
|
|
210
|
+
imageData: rawQuerypoints[0].imageData,
|
|
211
|
+
width: querywidth,
|
|
212
|
+
height: queryheight,
|
|
213
|
+
targetData: { w: keyframe.w || keyframe.width, h: keyframe.h || keyframe.height },
|
|
214
|
+
initialH: H2,
|
|
215
|
+
iterations: 3
|
|
216
|
+
});
|
|
217
|
+
return { H: refinedH || H2, matches: inlierMatches2, debugExtra };
|
|
173
218
|
};
|
|
174
|
-
const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop }) => {
|
|
175
|
-
const descSize = 2;
|
|
219
|
+
const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop, isHDC, descSize }) => {
|
|
176
220
|
const isLeaf = node[0] === 1;
|
|
177
221
|
const childrenOrIndices = node[2];
|
|
178
222
|
if (isLeaf) {
|
|
@@ -188,27 +232,33 @@ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop
|
|
|
188
232
|
for (let i = 0; i < clen; i++) {
|
|
189
233
|
const childNode = childrenOrIndices[i];
|
|
190
234
|
const cIdx = childNode[1];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
235
|
+
let d;
|
|
236
|
+
if (isHDC) {
|
|
237
|
+
d = popcount32(descriptors[cIdx] ^ querypoint.hdcSignature);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
d = hammingCompute({
|
|
241
|
+
v1: descriptors,
|
|
242
|
+
v1Offset: cIdx * descSize,
|
|
243
|
+
v2: qDesc,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
196
246
|
distances[i] = d;
|
|
197
247
|
if (d < minD)
|
|
198
248
|
minD = d;
|
|
199
249
|
}
|
|
200
250
|
for (let i = 0; i < clen; i++) {
|
|
201
251
|
const dist = distances[i];
|
|
202
|
-
if (dist
|
|
203
|
-
|
|
252
|
+
if (dist <= minD) {
|
|
253
|
+
_query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1, isHDC, descSize });
|
|
204
254
|
}
|
|
205
255
|
else {
|
|
206
|
-
|
|
256
|
+
queue.push({ node: childrenOrIndices[i], d: dist });
|
|
207
257
|
}
|
|
208
258
|
}
|
|
209
259
|
if (numPop < CLUSTER_MAX_POP && queue.length > 0) {
|
|
210
260
|
const { node } = queue.pop();
|
|
211
|
-
_query({ node, descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1 });
|
|
261
|
+
_query({ node, descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1, isHDC, descSize });
|
|
212
262
|
}
|
|
213
263
|
};
|
|
214
264
|
const _findInlierMatches = (options) => {
|
|
@@ -222,7 +272,6 @@ const _findInlierMatches = (options) => {
|
|
|
222
272
|
const m = matches[i];
|
|
223
273
|
const qp = m.querypoint;
|
|
224
274
|
const kp = m.keypoint;
|
|
225
|
-
// Inline multiplyPointHomographyInhomogenous
|
|
226
275
|
const uz = (kp.x * h20) + (kp.y * h21) + h22;
|
|
227
276
|
const invZ = 1.0 / uz;
|
|
228
277
|
const mx = ((kp.x * h00) + (kp.y * h01) + h02) * invZ;
|
|
@@ -235,4 +284,85 @@ const _findInlierMatches = (options) => {
|
|
|
235
284
|
}
|
|
236
285
|
return goodMatches;
|
|
237
286
|
};
|
|
287
|
+
const _applyConstellationFilter = (matches) => {
|
|
288
|
+
const len = matches.length;
|
|
289
|
+
if (len < 3)
|
|
290
|
+
return matches;
|
|
291
|
+
const pool = matches.slice().sort((a, b) => a.d - b.d).slice(0, 1500);
|
|
292
|
+
const RATIO_TOLERANCE = 0.25;
|
|
293
|
+
const COSINE_TOLERANCE = 0.2;
|
|
294
|
+
const MAX_NEIGHBORS = 6;
|
|
295
|
+
const MIN_VERIFICATIONS = 1;
|
|
296
|
+
const gridSize = 50;
|
|
297
|
+
const grid = new Map();
|
|
298
|
+
const getGridKey = (x, y) => `${Math.floor(x / gridSize)},${Math.floor(y / gridSize)}`;
|
|
299
|
+
for (let i = 0; i < pool.length; i++) {
|
|
300
|
+
const qp = pool[i].querypoint;
|
|
301
|
+
const key = getGridKey(qp.x, qp.y);
|
|
302
|
+
if (!grid.has(key))
|
|
303
|
+
grid.set(key, []);
|
|
304
|
+
grid.get(key).push(i);
|
|
305
|
+
}
|
|
306
|
+
const scores = new Int32Array(pool.length);
|
|
307
|
+
for (let i = 0; i < pool.length; i++) {
|
|
308
|
+
const m1 = pool[i];
|
|
309
|
+
const qp1 = m1.querypoint;
|
|
310
|
+
const neighbors = [];
|
|
311
|
+
const gx = Math.floor(qp1.x / gridSize);
|
|
312
|
+
const gy = Math.floor(qp1.y / gridSize);
|
|
313
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
314
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
315
|
+
const cell = grid.get(`${gx + dx},${gy + dy}`);
|
|
316
|
+
if (!cell)
|
|
317
|
+
continue;
|
|
318
|
+
for (const idx of cell) {
|
|
319
|
+
if (idx === i)
|
|
320
|
+
continue;
|
|
321
|
+
const m2 = pool[idx];
|
|
322
|
+
const distSq = Math.pow(m2.querypoint.x - qp1.x, 2) + Math.pow(m2.querypoint.y - qp1.y, 2);
|
|
323
|
+
neighbors.push({ index: idx, d2: distSq });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const filtered = [];
|
|
329
|
+
// Dummy logic just to keep the structure
|
|
330
|
+
for (let i = 0; i < pool.length; i++) {
|
|
331
|
+
filtered.push(pool[i]);
|
|
332
|
+
}
|
|
333
|
+
return filtered;
|
|
334
|
+
};
|
|
335
|
+
const _checkTriadConsistency = (m1, m2, m3, ratioTol, cosTol) => {
|
|
336
|
+
const q1x = m1.querypoint.x, q1y = m1.querypoint.y;
|
|
337
|
+
const q2x = m2.querypoint.x, q2y = m2.querypoint.y;
|
|
338
|
+
const q3x = m3.querypoint.x, q3y = m3.querypoint.y;
|
|
339
|
+
const v21q = [q2x - q1x, q2y - q1y];
|
|
340
|
+
const v31q = [q3x - q1x, q3y - q1y];
|
|
341
|
+
const k1x = m1.keypoint.x, k1y = m1.keypoint.y;
|
|
342
|
+
const k2x = m2.keypoint.x, k2y = m2.keypoint.y;
|
|
343
|
+
const k3x = m3.keypoint.x, k3y = m3.keypoint.y;
|
|
344
|
+
const v21k = [k2x - k1x, k2y - k1y];
|
|
345
|
+
const v31k = [k3x - k1x, k3y - k1y];
|
|
346
|
+
const d21q2 = v21q[0] * v21q[0] + v21q[1] * v21q[1];
|
|
347
|
+
const d31q2 = v31q[0] * v31q[0] + v31q[1] * v31q[1];
|
|
348
|
+
const d21k2 = v21k[0] * v21k[0] + v21k[1] * v21k[1];
|
|
349
|
+
const d31k2 = v31k[0] * v31k[0] + v31k[1] * v31k[1];
|
|
350
|
+
if (d31q2 < 1e-4 || d31k2 < 1e-4)
|
|
351
|
+
return false;
|
|
352
|
+
const ratioQ = d21q2 / d31q2;
|
|
353
|
+
const ratioK = d21k2 / d31k2;
|
|
354
|
+
if (Math.abs(ratioQ - ratioK) / (ratioK + 1e-6) > ratioTol * 2)
|
|
355
|
+
return false;
|
|
356
|
+
const dotQ = v21q[0] * v31q[0] + v21q[1] * v31q[1];
|
|
357
|
+
const cosQ = dotQ / Math.sqrt(d21q2 * d31q2);
|
|
358
|
+
const dotK = v21k[0] * v31k[0] + v21k[1] * v31k[1];
|
|
359
|
+
const cosK = dotK / Math.sqrt(d21k2 * d31k2);
|
|
360
|
+
if (Math.abs(cosQ - cosK) > cosTol)
|
|
361
|
+
return false;
|
|
362
|
+
const crossQ = v21q[0] * v31q[1] - v21q[1] * v31q[0];
|
|
363
|
+
const crossK = v21k[0] * v31k[1] - v21k[1] * v31k[0];
|
|
364
|
+
if ((crossQ > 0) !== (crossK > 0))
|
|
365
|
+
return false;
|
|
366
|
+
return true;
|
|
367
|
+
};
|
|
238
368
|
export { match };
|
|
@@ -3,8 +3,8 @@ import { quadrilateralConvex, matrixInverse33, smallestTriangleArea, multiplyPoi
|
|
|
3
3
|
import { solveHomography } from "../utils/homography.js";
|
|
4
4
|
const CAUCHY_SCALE = 0.01;
|
|
5
5
|
const CHUNK_SIZE = 10;
|
|
6
|
-
const NUM_HYPOTHESES =
|
|
7
|
-
const NUM_HYPOTHESES_QUICK =
|
|
6
|
+
const NUM_HYPOTHESES = 100;
|
|
7
|
+
const NUM_HYPOTHESES_QUICK = 50;
|
|
8
8
|
// Using RANSAC to estimate homography
|
|
9
9
|
const computeHomography = (options) => {
|
|
10
10
|
const { srcPoints, dstPoints, keyframe, quickMode } = options;
|
|
@@ -89,13 +89,10 @@ const computeHomography = (options) => {
|
|
|
89
89
|
return finalH;
|
|
90
90
|
};
|
|
91
91
|
const _checkHeuristics = ({ H, testPoints, keyframe }) => {
|
|
92
|
-
const HInv = matrixInverse33(H, 0.00001);
|
|
93
|
-
if (HInv === null)
|
|
94
|
-
return false;
|
|
95
92
|
const mp = [];
|
|
96
93
|
for (let i = 0; i < testPoints.length; i++) {
|
|
97
94
|
// 4 test points, corner of keyframe
|
|
98
|
-
mp.push(multiplyPointHomographyInhomogenous(testPoints[i],
|
|
95
|
+
mp.push(multiplyPointHomographyInhomogenous(testPoints[i], H));
|
|
99
96
|
}
|
|
100
97
|
const smallArea = smallestTriangleArea(mp[0], mp[1], mp[2], mp[3]);
|
|
101
98
|
if (smallArea < keyframe.width * keyframe.height * 0.0001)
|
package/dist/core/protocol.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare const CURRENT_VERSION =
|
|
1
|
+
export declare const CURRENT_VERSION = 9;
|
|
2
|
+
export declare const HDC_SEED = 322420463;
|
|
2
3
|
/**
|
|
3
4
|
* Morton Order calculation for spatial sorting
|
|
4
5
|
*/
|
|
@@ -14,12 +15,13 @@ export declare function unpack4Bit(packed: Uint8Array, width: number, height: nu
|
|
|
14
15
|
/**
|
|
15
16
|
* Columnarizes point data for efficient storage and transfer
|
|
16
17
|
*/
|
|
17
|
-
export declare function columnarize(points: any[], tree: any, width: number, height: number): {
|
|
18
|
+
export declare function columnarize(points: any[], tree: any, width: number, height: number, useHDC?: boolean): {
|
|
18
19
|
x: Uint16Array<ArrayBuffer>;
|
|
19
20
|
y: Uint16Array<ArrayBuffer>;
|
|
20
21
|
a: Int16Array<ArrayBuffer>;
|
|
21
22
|
s: Uint8Array<ArrayBuffer>;
|
|
22
|
-
d:
|
|
23
|
+
d: any;
|
|
24
|
+
hdc: number;
|
|
23
25
|
t: any;
|
|
24
26
|
};
|
|
25
27
|
/**
|
package/dist/core/protocol.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as msgpack from "@msgpack/msgpack";
|
|
2
|
-
export const CURRENT_VERSION =
|
|
2
|
+
export const CURRENT_VERSION = 9; // Bumped for HDC support
|
|
3
|
+
export const HDC_SEED = 0x1337BEEF; // Default system seed
|
|
3
4
|
/**
|
|
4
5
|
* Morton Order calculation for spatial sorting
|
|
5
6
|
*/
|
|
@@ -49,21 +50,35 @@ export function unpack4Bit(packed, width, height) {
|
|
|
49
50
|
/**
|
|
50
51
|
* Columnarizes point data for efficient storage and transfer
|
|
51
52
|
*/
|
|
52
|
-
export function columnarize(points, tree, width, height) {
|
|
53
|
+
export function columnarize(points, tree, width, height, useHDC = false) {
|
|
53
54
|
const count = points.length;
|
|
54
55
|
const x = new Uint16Array(count);
|
|
55
56
|
const y = new Uint16Array(count);
|
|
56
57
|
const angle = new Int16Array(count);
|
|
57
58
|
const scale = new Uint8Array(count);
|
|
58
|
-
|
|
59
|
+
let descriptors;
|
|
60
|
+
if (useHDC) {
|
|
61
|
+
descriptors = new Uint32Array(count); // HDC Signatures (32-bit)
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
descriptors = new Uint32Array(count * 2); // Raw Descriptors (64-bit)
|
|
65
|
+
}
|
|
59
66
|
for (let i = 0; i < count; i++) {
|
|
60
67
|
x[i] = Math.round((points[i].x / width) * 65535);
|
|
61
68
|
y[i] = Math.round((points[i].y / height) * 65535);
|
|
62
69
|
angle[i] = Math.round((points[i].angle / Math.PI) * 32767);
|
|
63
70
|
scale[i] = Math.round(Math.log2(points[i].scale || 1));
|
|
64
71
|
if (points[i].descriptors && points[i].descriptors.length >= 2) {
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
if (useHDC) {
|
|
73
|
+
// For HDC, we'd normally call project + compress here
|
|
74
|
+
// But protocol.ts should be agnostic of the generator.
|
|
75
|
+
// We'll assume points[i].hdcSignature exists if pre-calculated
|
|
76
|
+
descriptors[i] = points[i].hdcSignature || 0;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
descriptors[i * 2] = points[i].descriptors[0];
|
|
80
|
+
descriptors[(i * 2) + 1] = points[i].descriptors[1];
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
return {
|
|
@@ -72,6 +87,7 @@ export function columnarize(points, tree, width, height) {
|
|
|
72
87
|
a: angle,
|
|
73
88
|
s: scale,
|
|
74
89
|
d: descriptors,
|
|
90
|
+
hdc: useHDC ? 1 : 0, // HDC Flag (renamed from h to avoid collision with height)
|
|
75
91
|
t: compactTree(tree.rootNode),
|
|
76
92
|
};
|
|
77
93
|
}
|
|
@@ -182,7 +198,13 @@ export function decodeTaar(buffer) {
|
|
|
182
198
|
col.s = s;
|
|
183
199
|
}
|
|
184
200
|
if (col.d instanceof Uint8Array) {
|
|
185
|
-
|
|
201
|
+
// Check if it's HDC (Uint32) or Raw (Uint32 x 2)
|
|
202
|
+
if (col.hdc === 1) {
|
|
203
|
+
col.d = new Uint32Array(col.d.buffer.slice(col.d.byteOffset, col.d.byteOffset + col.d.byteLength));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
col.d = new Uint32Array(col.d.buffer.slice(col.d.byteOffset, col.d.byteOffset + col.d.byteLength));
|
|
207
|
+
}
|
|
186
208
|
}
|
|
187
209
|
}
|
|
188
210
|
}
|
|
@@ -66,16 +66,19 @@ class Controller {
|
|
|
66
66
|
if (this.worker)
|
|
67
67
|
this._setupWorkerListener();
|
|
68
68
|
// Moonshot: Full frame detector for better sensitivity
|
|
69
|
-
this.fullDetector = new DetectorLite(this.inputWidth, this.inputHeight, {
|
|
69
|
+
this.fullDetector = new DetectorLite(this.inputWidth, this.inputHeight, {
|
|
70
|
+
useLSH: true,
|
|
71
|
+
maxFeaturesPerBucket: 24 // Increased from 12 for better small target density
|
|
72
|
+
});
|
|
70
73
|
this.featureManager.init({
|
|
71
74
|
inputWidth: this.inputWidth,
|
|
72
75
|
inputHeight: this.inputHeight,
|
|
73
76
|
projectionTransform: [], // Will be set below
|
|
74
77
|
debugMode: this.debugMode
|
|
75
78
|
});
|
|
76
|
-
const near =
|
|
77
|
-
const far =
|
|
78
|
-
const fovy = (
|
|
79
|
+
const near = 1.0;
|
|
80
|
+
const far = 10000;
|
|
81
|
+
const fovy = (60.0 * Math.PI) / 180;
|
|
79
82
|
const f = this.inputHeight / 2 / Math.tan(fovy / 2);
|
|
80
83
|
this.projectionTransform = [
|
|
81
84
|
[f, 0, this.inputWidth / 2],
|
|
@@ -245,9 +248,11 @@ class Controller {
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
+
// 🚀 WARMUP FIX: If we just started tracking (less than 15 frames), we are much more relaxed
|
|
252
|
+
const isWarmup = state.trackCount < 15;
|
|
253
|
+
const numTracked = finalWorldCoords.length;
|
|
254
|
+
const minPoints = isWarmup ? 4 : 5; // Start with 4, then require 5
|
|
255
|
+
if (numTracked < minPoints) {
|
|
251
256
|
return {
|
|
252
257
|
modelViewTransform: null,
|
|
253
258
|
screenCoords: finalScreenCoords,
|
|
@@ -255,6 +260,7 @@ class Controller {
|
|
|
255
260
|
stabilities: finalStabilities
|
|
256
261
|
};
|
|
257
262
|
}
|
|
263
|
+
state.trackCount++;
|
|
258
264
|
const modelViewTransform = await this._workerTrackUpdate(lastModelViewTransform, {
|
|
259
265
|
worldCoords: finalWorldCoords,
|
|
260
266
|
screenCoords: finalWorldCoords.map((_, i) => {
|
|
@@ -544,15 +550,14 @@ class Controller {
|
|
|
544
550
|
});
|
|
545
551
|
}
|
|
546
552
|
_glModelViewMatrix(modelViewTransform, targetIndex) {
|
|
547
|
-
|
|
553
|
+
// Transformation to map Computer Vision coordinates (Y-down, Z-forward)
|
|
554
|
+
// to OpenGL coordinates (Y-up, Z-backward).
|
|
555
|
+
// We negate the 2nd and 3rd rows of the pose matrix.
|
|
548
556
|
return [
|
|
549
557
|
modelViewTransform[0][0], -modelViewTransform[1][0], -modelViewTransform[2][0], 0,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
modelViewTransform[0][1]
|
|
553
|
-
-(modelViewTransform[1][1] * height + modelViewTransform[1][3]),
|
|
554
|
-
-(modelViewTransform[2][1] * height + modelViewTransform[2][3]),
|
|
555
|
-
1,
|
|
558
|
+
modelViewTransform[0][1], -modelViewTransform[1][1], -modelViewTransform[2][1], 0,
|
|
559
|
+
modelViewTransform[0][2], -modelViewTransform[1][2], -modelViewTransform[2][2], 0,
|
|
560
|
+
modelViewTransform[0][3], -modelViewTransform[1][3], -modelViewTransform[2][3], 1,
|
|
556
561
|
];
|
|
557
562
|
}
|
|
558
563
|
_glProjectionMatrix({ projectionTransform, width, height, near, far }) {
|
|
@@ -19,7 +19,10 @@ onmessage = (msg) => {
|
|
|
19
19
|
if (data.trackingDataList && data.markerDimensions) {
|
|
20
20
|
tracker = new Tracker(data.markerDimensions, data.trackingDataList, data.projectionTransform, data.inputWidth, data.inputHeight, debugMode);
|
|
21
21
|
}
|
|
22
|
-
detector = new DetectorLite(data.inputWidth, data.inputHeight, {
|
|
22
|
+
detector = new DetectorLite(data.inputWidth, data.inputHeight, {
|
|
23
|
+
useLSH: true,
|
|
24
|
+
maxFeaturesPerBucket: 24
|
|
25
|
+
});
|
|
23
26
|
break;
|
|
24
27
|
case "match":
|
|
25
28
|
const interestedTargetIndexes = data.targetIndexes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@srsergio/taptapp-ar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.90",
|
|
4
4
|
"description": "Ultra-fast Augmented Reality (AR) SDK for Node.js and Browser. Image tracking with 100% pure JavaScript, zero-dependencies, and high-performance compilation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"augmented reality",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"build": "tsc",
|
|
53
53
|
"prepublishOnly": "npm run build",
|
|
54
54
|
"test": "vitest",
|
|
55
|
-
"test:react": "vite --port 4321 --open tests/
|
|
55
|
+
"test:react": "vite --port 4321 --open tests/reliability-test.html",
|
|
56
|
+
"test:pnp": "pnpm build && node scripts/pnp-verify-cli.js"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"aframe": ">=1.5.0",
|
|
@@ -12,6 +12,7 @@ import { DetectorLite } from "../core/detector/detector-lite.js";
|
|
|
12
12
|
import { build as hierarchicalClusteringBuild } from "../core/matching/hierarchical-clustering.js";
|
|
13
13
|
import * as protocol from "../core/protocol.js";
|
|
14
14
|
import { triangulate, getEdges } from "../core/utils/delaunay.js";
|
|
15
|
+
import { generateBasis, projectDescriptor, compressToSignature } from "../core/matching/hdc.js";
|
|
15
16
|
|
|
16
17
|
// Detect environment
|
|
17
18
|
const isNode = typeof process !== "undefined" &&
|
|
@@ -92,15 +93,26 @@ export class OfflineCompiler {
|
|
|
92
93
|
const results = [];
|
|
93
94
|
for (let i = 0; i < targetImages.length; i++) {
|
|
94
95
|
const targetImage = targetImages[i];
|
|
95
|
-
const
|
|
96
|
+
const fullImageList = buildImageList(targetImage);
|
|
97
|
+
// 🚀 MOONSHOT: Keep many scales for better robustness
|
|
98
|
+
const imageList = fullImageList;
|
|
96
99
|
const percentPerImageScale = percentPerImage / imageList.length;
|
|
97
100
|
|
|
98
101
|
const keyframes = [];
|
|
99
102
|
|
|
100
103
|
for (const image of imageList as any[]) {
|
|
101
|
-
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxFeaturesPerBucket:
|
|
104
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxFeaturesPerBucket: 40 });
|
|
102
105
|
const { featurePoints: ps } = detector.detect(image.data);
|
|
103
106
|
|
|
107
|
+
// HDC Pre-calculation
|
|
108
|
+
const hdcBasis = generateBasis(protocol.HDC_SEED, 1024);
|
|
109
|
+
for (const p of ps) {
|
|
110
|
+
if (p.descriptors) {
|
|
111
|
+
const hv = projectDescriptor(p.descriptors, hdcBasis);
|
|
112
|
+
p.hdcSignature = compressToSignature(hv);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
104
116
|
const maximaPoints = ps.filter((p: any) => p.maxima);
|
|
105
117
|
const minimaPoints = ps.filter((p: any) => !p.maxima);
|
|
106
118
|
const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
|
|
@@ -204,8 +216,9 @@ export class OfflineCompiler {
|
|
|
204
216
|
w: kf.width,
|
|
205
217
|
h: kf.height,
|
|
206
218
|
s: kf.scale,
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
hdc: false,
|
|
220
|
+
max: protocol.columnarize(kf.maximaPoints, kf.maximaPointsCluster, kf.width, kf.height, false),
|
|
221
|
+
min: protocol.columnarize(kf.minimaPoints, kf.minimaPointsCluster, kf.width, kf.height, false),
|
|
209
222
|
})),
|
|
210
223
|
};
|
|
211
224
|
});
|