@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.
Files changed (41) hide show
  1. package/README.md +17 -10
  2. package/dist/compiler/offline-compiler.js +16 -4
  3. package/dist/core/detector/detector-lite.d.ts +1 -0
  4. package/dist/core/detector/detector-lite.js +31 -15
  5. package/dist/core/estimation/estimate.d.ts +7 -0
  6. package/dist/core/estimation/estimate.js +13 -48
  7. package/dist/core/estimation/morph-refinement.d.ts +8 -0
  8. package/dist/core/estimation/morph-refinement.js +116 -0
  9. package/dist/core/estimation/pnp-solver.d.ts +5 -0
  10. package/dist/core/estimation/pnp-solver.js +109 -0
  11. package/dist/core/input-loader.js +19 -2
  12. package/dist/core/matching/hdc.d.ts +27 -0
  13. package/dist/core/matching/hdc.js +102 -0
  14. package/dist/core/matching/hierarchical-clustering.d.ts +1 -3
  15. package/dist/core/matching/hierarchical-clustering.js +30 -29
  16. package/dist/core/matching/hough.js +12 -11
  17. package/dist/core/matching/matcher.d.ts +4 -0
  18. package/dist/core/matching/matcher.js +23 -8
  19. package/dist/core/matching/matching.d.ts +22 -2
  20. package/dist/core/matching/matching.js +169 -39
  21. package/dist/core/matching/ransacHomography.js +3 -6
  22. package/dist/core/protocol.d.ts +5 -3
  23. package/dist/core/protocol.js +28 -6
  24. package/dist/runtime/controller.js +19 -14
  25. package/dist/runtime/controller.worker.js +4 -1
  26. package/package.json +3 -2
  27. package/src/compiler/offline-compiler.ts +17 -4
  28. package/src/core/detector/detector-lite.js +32 -15
  29. package/src/core/estimation/estimate.js +14 -63
  30. package/src/core/estimation/morph-refinement.js +139 -0
  31. package/src/core/estimation/pnp-solver.js +131 -0
  32. package/src/core/input-loader.js +21 -2
  33. package/src/core/matching/hdc.ts +117 -0
  34. package/src/core/matching/hierarchical-clustering.js +30 -29
  35. package/src/core/matching/hough.js +12 -11
  36. package/src/core/matching/matcher.js +27 -9
  37. package/src/core/matching/matching.js +192 -39
  38. package/src/core/matching/ransacHomography.js +3 -6
  39. package/src/core/protocol.ts +26 -6
  40. package/src/runtime/controller.ts +20 -14
  41. package/src/runtime/controller.worker.js +4 -1
@@ -1,20 +1,18 @@
1
1
  import { compute64 as hammingCompute64 } from "./hamming-distance.js";
2
2
  import { createRandomizer } from "../utils/randomizer.js";
3
3
 
4
- const MIN_FEATURE_PER_NODE = 32; // Increased from 16 for speed
5
- const NUM_ASSIGNMENT_HYPOTHESES = 12; // Reduced from 16 for speed
4
+ const MIN_FEATURE_PER_NODE = 32;
5
+ const NUM_ASSIGNMENT_HYPOTHESES = 12;
6
6
  const NUM_CENTERS = 8;
7
7
 
8
- /**
9
- * 🚀 Moonshot Optimized K-Medoids
10
- *
11
- * Major Optimizations:
12
- * 1. Flattened Memory: Operates on a single Uint32Array block instead of objects.
13
- * 2. Zero Property Access: Avoids .descriptors lookup in the tightest loop.
14
- * 3. Cache-Friendly: Accesses contiguous descriptor data.
15
- */
8
+ export function popcount32(n) {
9
+ n = n - ((n >> 1) & 0x55555555);
10
+ n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
11
+ return (((n + (n >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
12
+ }
13
+
16
14
  const _computeKMedoids = (options) => {
17
- const { descriptors, pointIndexes, randomizer } = options;
15
+ const { descriptors, pointIndexes, randomizer, useHDC } = options;
18
16
  const numPointIndexes = pointIndexes.length;
19
17
 
20
18
  const randomPointIndexes = new Int32Array(numPointIndexes);
@@ -25,13 +23,11 @@ const _computeKMedoids = (options) => {
25
23
  let bestSumD = Number.MAX_SAFE_INTEGER;
26
24
  let bestAssignment = null;
27
25
 
28
- // Pre-fetch centers indices to avoid nested index lookups
29
26
  const centerPointIndices = new Int32Array(NUM_CENTERS);
30
27
 
31
28
  for (let i = 0; i < NUM_ASSIGNMENT_HYPOTHESES; i++) {
32
29
  randomizer.arrayShuffle({ arr: randomPointIndexes, sampleSize: NUM_CENTERS });
33
30
 
34
- // Set centers for this hypothesis
35
31
  for (let k = 0; k < NUM_CENTERS; k++) {
36
32
  centerPointIndices[k] = pointIndexes[randomPointIndexes[k]];
37
33
  }
@@ -41,17 +37,19 @@ const _computeKMedoids = (options) => {
41
37
 
42
38
  for (let j = 0; j < numPointIndexes; j++) {
43
39
  const pIdx = pointIndexes[j];
44
- const pOffset = pIdx * 2;
45
40
 
46
- let bestD = 255; // Max possible Hamming for 64-bit is 64, but let's be safe
41
+ let bestD = 255;
47
42
  let bestCenterIdx = -1;
48
43
 
49
44
  for (let k = 0; k < NUM_CENTERS; k++) {
50
45
  const cIdx = centerPointIndices[k];
51
- const cOffset = cIdx * 2;
52
46
 
53
- // DIRECT CALL TO INLINED HAMMING
54
- const d = hammingCompute64(descriptors, pOffset, descriptors, cOffset);
47
+ let d;
48
+ if (useHDC) {
49
+ d = popcount32(descriptors[pIdx] ^ descriptors[cIdx]);
50
+ } else {
51
+ d = hammingCompute64(descriptors, pIdx * 2, descriptors, cIdx * 2);
52
+ }
55
53
 
56
54
  if (d < bestD) {
57
55
  bestCenterIdx = randomPointIndexes[k];
@@ -70,20 +68,21 @@ const _computeKMedoids = (options) => {
70
68
  return bestAssignment;
71
69
  };
72
70
 
73
- /**
74
- * Build hierarchical clusters
75
- */
76
71
  const build = ({ points }) => {
77
72
  const numPoints = points.length;
78
73
  if (numPoints === 0) return { rootNode: { leaf: true, pointIndexes: [], centerPointIndex: null } };
79
74
 
80
- // 🚀 MOONSHOT: Flatten all descriptors into a single Uint32Array
81
- // This is the key to sub-second performance.
82
- const descriptors = new Uint32Array(numPoints * 2);
75
+ const useHDC = points[0] && points[0].hdcSignature !== undefined;
76
+ const descriptors = new Uint32Array(useHDC ? numPoints : numPoints * 2);
77
+
83
78
  for (let i = 0; i < numPoints; i++) {
84
- const d = points[i].descriptors;
85
- descriptors[i * 2] = d[0];
86
- descriptors[i * 2 + 1] = d[1];
79
+ if (useHDC) {
80
+ descriptors[i] = points[i].hdcSignature;
81
+ } else {
82
+ const d = points[i].descriptors;
83
+ descriptors[i * 2] = d[0];
84
+ descriptors[i * 2 + 1] = d[1];
85
+ }
87
86
  }
88
87
 
89
88
  const pointIndexes = new Int32Array(numPoints);
@@ -98,12 +97,13 @@ const build = ({ points }) => {
98
97
  pointIndexes,
99
98
  centerPointIndex: null,
100
99
  randomizer,
100
+ useHDC
101
101
  });
102
102
  return { rootNode };
103
103
  };
104
104
 
105
105
  const _build = (options) => {
106
- const { descriptors, pointIndexes, centerPointIndex, randomizer } = options;
106
+ const { descriptors, pointIndexes, centerPointIndex, randomizer, useHDC } = options;
107
107
  const numPoints = pointIndexes.length;
108
108
 
109
109
  let isLeaf = false;
@@ -113,7 +113,7 @@ const _build = (options) => {
113
113
 
114
114
  const clusters = new Map();
115
115
  if (!isLeaf) {
116
- const assignment = _computeKMedoids({ descriptors, pointIndexes, randomizer });
116
+ const assignment = _computeKMedoids({ descriptors, pointIndexes, randomizer, useHDC });
117
117
 
118
118
  for (let i = 0; i < assignment.length; i++) {
119
119
  const centerIdx = pointIndexes[assignment[i]];
@@ -150,6 +150,7 @@ const _build = (options) => {
150
150
  pointIndexes: new Int32Array(clusterPoints),
151
151
  centerPointIndex: cIdx,
152
152
  randomizer,
153
+ useHDC
153
154
  }),
154
155
  );
155
156
  }
@@ -9,9 +9,9 @@ const computeHoughMatches = (options) => {
9
9
  const maxY = queryheight * 1.2;
10
10
  const minY = -maxY;
11
11
  const numAngleBins = 12;
12
- const numScaleBins = 10;
13
- const minScale = -1;
14
- const maxScale = 1;
12
+ const numScaleBins = 12; // 🚀 Increased bins
13
+ const minScale = -2; // 📐 Support 1% scale (10^-2)
14
+ const maxScale = 1; // 📐 Support 1000% scale (10^1)
15
15
  const scaleK = 10.0;
16
16
  const scaleOneOverLogK = 1.0 / Math.log(scaleK);
17
17
  const maxDim = Math.max(keywidth, keyheight);
@@ -38,9 +38,9 @@ const computeHoughMatches = (options) => {
38
38
  Math.floor(projectedDims.length / 2) - (projectedDims.length % 2 == 0 ? 1 : 0) - 1
39
39
  ];
40
40
 
41
- const binSize = 0.25 * medianProjectedDim;
42
- const numXBins = Math.max(5, Math.ceil((maxX - minX) / binSize));
43
- const numYBins = Math.max(5, Math.ceil((maxY - minY) / binSize));
41
+ const binSize = Math.max(20, 0.25 * medianProjectedDim); // 🚀 Ensure bins aren't too small for noise
42
+ const numXBins = Math.max(5, Math.min(40, Math.ceil((maxX - minX) / binSize))); // 🎯 Cap bins to keep voting dense
43
+ const numYBins = Math.max(5, Math.min(40, Math.ceil((maxY - minY) / binSize)));
44
44
 
45
45
  const numXYBins = numXBins * numYBins;
46
46
  const numXYAngleBins = numXYBins * numAngleBins;
@@ -152,26 +152,27 @@ const computeHoughMatches = (options) => {
152
152
  (maxVoteIndex - binX - binY * numXBins - binAngle * numXYBins) / numXYAngleBins,
153
153
  );
154
154
 
155
- //console.log("hough voted: ", {binX, binY, binAngle, binScale, maxVoteIndex});
155
+ // console.log(`[Hough] Peak votes: ${maxVotes} out of ${matches.length} matches.`);
156
156
 
157
157
  const houghMatches = [];
158
+ const relaxedDelta = 2.0; // 🚀 Increased for better cluster robustness
158
159
  for (let i = 0; i < matches.length; i++) {
159
160
  if (!querypointValids[i]) continue;
160
161
 
161
162
  const queryBins = querypointBinLocations[i];
162
163
  // compute bin difference
163
164
  const distBinX = Math.abs(queryBins.binX - (binX + 0.5));
164
- if (distBinX >= kHoughBinDelta) continue;
165
+ if (distBinX >= relaxedDelta) continue;
165
166
 
166
167
  const distBinY = Math.abs(queryBins.binY - (binY + 0.5));
167
- if (distBinY >= kHoughBinDelta) continue;
168
+ if (distBinY >= relaxedDelta) continue;
168
169
 
169
170
  const distBinScale = Math.abs(queryBins.binScale - (binScale + 0.5));
170
- if (distBinScale >= kHoughBinDelta) continue;
171
+ if (distBinScale >= relaxedDelta) continue;
171
172
 
172
173
  const temp = Math.abs(queryBins.binAngle - (binAngle + 0.5));
173
174
  const distBinAngle = Math.min(temp, numAngleBins - temp);
174
- if (distBinAngle >= kHoughBinDelta) continue;
175
+ if (distBinAngle >= relaxedDelta) continue;
175
176
 
176
177
  houghMatches.push(matches[i]);
177
178
  }
@@ -9,36 +9,47 @@ class Matcher {
9
9
 
10
10
  matchDetection(keyframes, featurePoints) {
11
11
  let debugExtra = { frames: [] };
12
-
13
12
  let bestResult = null;
14
- for (let i = 0; i < keyframes.length; i++) {
13
+
14
+ // keyframes is actually the matchingData array for a single target
15
+ if (!keyframes || !Array.isArray(keyframes)) {
16
+ return { targetIndex: -1, keyframeIndex: -1, debugExtra };
17
+ }
18
+
19
+ for (let j = 0; j < keyframes.length; j++) {
15
20
  const {
16
21
  H,
17
22
  matches,
18
23
  debugExtra: frameDebugExtra,
19
24
  } = match({
20
- keyframe: keyframes[i],
25
+ keyframe: keyframes[j],
21
26
  querypoints: featurePoints,
22
27
  querywidth: this.queryWidth,
23
28
  queryheight: this.queryHeight,
24
29
  debugMode: this.debugMode,
25
30
  });
26
- debugExtra.frames.push(frameDebugExtra);
31
+
32
+ if (frameDebugExtra) {
33
+ frameDebugExtra.keyframeIndex = j;
34
+ debugExtra.frames.push(frameDebugExtra);
35
+ }
27
36
 
28
37
  if (H) {
29
38
  if (bestResult === null || bestResult.matches.length < matches.length) {
30
- bestResult = { keyframeIndex: i, H, matches };
39
+ bestResult = { keyframeIndex: j, H, matches };
31
40
  }
32
41
  }
33
42
  }
34
43
 
35
44
  if (bestResult === null) {
36
- return { keyframeIndex: -1, debugExtra };
45
+ return { targetIndex: -1, keyframeIndex: -1, debugExtra };
37
46
  }
38
47
 
39
48
  const screenCoords = [];
40
49
  const worldCoords = [];
41
50
  const keyframe = keyframes[bestResult.keyframeIndex];
51
+ const scale = keyframe.s || keyframe.scale || 1.0;
52
+
42
53
  for (let i = 0; i < bestResult.matches.length; i++) {
43
54
  const querypoint = bestResult.matches[i].querypoint;
44
55
  const keypoint = bestResult.matches[i].keypoint;
@@ -47,12 +58,19 @@ class Matcher {
47
58
  y: querypoint.y,
48
59
  });
49
60
  worldCoords.push({
50
- x: (keypoint.x + 0.5) / (keyframe.s || keyframe.scale || 1.0),
51
- y: (keypoint.y + 0.5) / (keyframe.s || keyframe.scale || 1.0),
61
+ x: (keypoint.x + 0.5) / scale,
62
+ y: (keypoint.y + 0.5) / scale,
52
63
  z: 0,
53
64
  });
54
65
  }
55
- return { screenCoords, worldCoords, keyframeIndex: bestResult.keyframeIndex, debugExtra };
66
+ return {
67
+ screenCoords,
68
+ worldCoords,
69
+ targetIndex: -1, // Caller knows the targetIndex
70
+ keyframeIndex: bestResult.keyframeIndex,
71
+ H: bestResult.H,
72
+ debugExtra
73
+ };
56
74
  }
57
75
  }
58
76
 
@@ -3,21 +3,33 @@ 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
+ import { refineWithMorphology } from "../estimation/morph-refinement.js";
7
+ import { popcount32 } from "./hierarchical-clustering.js";
6
8
 
7
- const INLIER_THRESHOLD = 5.0; // Tightened from 10 to 5 for better precision
8
- const MIN_NUM_INLIERS = 8; // Restored to 8
9
- const CLUSTER_MAX_POP = 20;
10
- const HAMMING_THRESHOLD = 0.85; // Relaxed from 0.8 back to 0.85
9
+ const INLIER_THRESHOLD = 15.0; // 🚀 More tolerance for initial noise
10
+ const MIN_NUM_INLIERS = 6; // 🎯 Lowered from 8 to catch the marker earlier
11
+ const CLUSTER_MAX_POP = 25;
12
+ const HAMMING_THRESHOLD = 0.85;
13
+ const HDC_RATIO_THRESHOLD = 0.85;
14
+ const MAX_MATCH_QUERY_POINTS = 800; // 🚀 Increased to handle 3000+ points detected
11
15
 
12
16
  // match list of querpoints against pre-built list of keyframes
13
- const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) => {
17
+ const match = ({ keyframe, querypoints: rawQuerypoints, querywidth, queryheight, debugMode }) => {
14
18
  let debugExtra = {};
15
19
 
20
+ // 🎯 Performance Optimizer: Use only the most "salient" points (highest response)
21
+ const querypoints = rawQuerypoints.length > MAX_MATCH_QUERY_POINTS
22
+ ? [...rawQuerypoints].sort((a, b) => (b.score || b.response || 0) - (a.score || a.response || 0)).slice(0, MAX_MATCH_QUERY_POINTS)
23
+ : rawQuerypoints;
24
+
16
25
  const matches = [];
17
26
  const qlen = querypoints.length;
18
27
  const kmax = keyframe.max;
19
28
  const kmin = keyframe.min;
20
- const descSize = 2; // Protocol V6: 64-bit LSH (2 x 32-bit)
29
+ const isHDC = keyframe.hdc === true || (kmax && kmax.hdc === 1);
30
+ const descSize = isHDC ? 1 : 2;
31
+
32
+ const currentRatioThreshold = isHDC ? HDC_RATIO_THRESHOLD : HAMMING_THRESHOLD;
21
33
 
22
34
  for (let j = 0; j < qlen; j++) {
23
35
  const querypoint = querypoints[j];
@@ -28,14 +40,15 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
28
40
  const keypointIndexes = [];
29
41
  const queue = new TinyQueue([], (a1, a2) => a1.d - a2.d);
30
42
 
31
- // query potential candidates from the columnar tree
32
43
  _query({
33
44
  node: rootNode,
34
45
  descriptors: col.d,
35
46
  querypoint,
36
47
  queue,
37
48
  keypointIndexes,
38
- numPop: 0
49
+ numPop: 0,
50
+ isHDC,
51
+ descSize
39
52
  });
40
53
 
41
54
  let bestIndex = -1;
@@ -48,8 +61,12 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
48
61
  for (let k = 0; k < keypointIndexes.length; k++) {
49
62
  const idx = keypointIndexes[k];
50
63
 
51
- // Use offsets based on detected descriptor size
52
- const d = hammingCompute({ v1: cDesc, v1Offset: idx * descSize, v2: qDesc });
64
+ let d;
65
+ if (isHDC) {
66
+ d = popcount32(cDesc[idx] ^ querypoint.hdcSignature);
67
+ } else {
68
+ d = hammingCompute({ v1: cDesc, v1Offset: idx * descSize, v2: qDesc });
69
+ }
53
70
 
54
71
  if (d < bestD1) {
55
72
  bestD2 = bestD1;
@@ -61,7 +78,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
61
78
  }
62
79
 
63
80
  if (bestIndex !== -1) {
64
- if (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < HAMMING_THRESHOLD) {
81
+ if (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < currentRatioThreshold) {
65
82
  matches.push({
66
83
  querypoint,
67
84
  keypoint: {
@@ -69,28 +86,45 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
69
86
  y: col.y[bestIndex],
70
87
  angle: col.a[bestIndex],
71
88
  scale: col.s ? col.s[bestIndex] : keyframe.s
72
- }
89
+ },
90
+ d: bestD1
73
91
  });
74
92
  }
75
93
  }
76
94
  }
77
95
 
78
- if (matches.length < MIN_NUM_INLIERS) return { debugExtra };
96
+
97
+ if (matches.length < MIN_NUM_INLIERS) {
98
+ return { debugExtra };
99
+ }
100
+
101
+ // Debug: Log Hamming results
102
+ if (Math.random() < 0.1 && debugMode) {
103
+ console.log(`MATCH_DL: Hamming found ${matches.length} initial matches`);
104
+ }
105
+
106
+ // 🌌 Moonshot: Constellation matching disabled for performance calibration
107
+ const constellationMatches = matches;
108
+ if (debugMode) debugExtra.constellationMatches = constellationMatches;
79
109
 
80
110
  const houghMatches = computeHoughMatches({
81
- keywidth: keyframe.w,
82
- keyheight: keyframe.h,
111
+ keywidth: keyframe.w || keyframe.width,
112
+ keyheight: keyframe.h || keyframe.height,
83
113
  querywidth,
84
114
  queryheight,
85
- matches,
115
+ matches: constellationMatches,
86
116
  });
87
117
 
88
118
  if (debugMode) debugExtra.houghMatches = houghMatches;
89
119
 
120
+ if (houghMatches.length < MIN_NUM_INLIERS) {
121
+ return { debugExtra };
122
+ }
123
+
90
124
  const H = computeHomography({
91
125
  srcPoints: houghMatches.map((m) => [m.keypoint.x, m.keypoint.y]),
92
126
  dstPoints: houghMatches.map((m) => [m.querypoint.x, m.querypoint.y]),
93
- keyframe: { width: keyframe.w, height: keyframe.h },
127
+ keyframe: { width: keyframe.w || keyframe.width, height: keyframe.h || keyframe.height },
94
128
  });
95
129
 
96
130
  if (H === null) {
@@ -104,11 +138,17 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
104
138
  });
105
139
 
106
140
  if (debugMode) debugExtra.inlierMatches = inlierMatches;
107
- if (inlierMatches.length < MIN_NUM_INLIERS) return { debugExtra };
141
+ if (inlierMatches.length < MIN_NUM_INLIERS) {
142
+ return { debugExtra };
143
+ }
144
+
145
+ if (debugMode && Math.random() < 0.02) {
146
+ console.log(`MATCH: Homography success with ${inlierMatches.length} inliers`);
147
+ }
148
+
108
149
 
109
- // Second pass with homography guided matching
110
150
  const HInv = matrixInverse33(H, 0.00001);
111
- const dThreshold2 = 100; // 10 * 10
151
+ const dThreshold2 = 100;
112
152
  const matches2 = [];
113
153
 
114
154
  const hi00 = HInv[0], hi01 = HInv[1], hi02 = HInv[2];
@@ -119,7 +159,6 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
119
159
  const querypoint = querypoints[j];
120
160
  const qx = querypoint.x, qy = querypoint.y;
121
161
 
122
- // Inline multiplyPointHomographyInhomogenous
123
162
  const uz = (qx * hi20) + (qy * hi21) + hi22;
124
163
  const invZ = 1.0 / uz;
125
164
  const mapX = ((qx * hi00) + (qy * hi01) + hi02) * invZ;
@@ -142,7 +181,12 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
142
181
 
143
182
  if (d2 > dThreshold2) continue;
144
183
 
145
- const d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
184
+ let d;
185
+ if (isHDC) {
186
+ d = popcount32(cd[k] ^ querypoint.hdcSignature);
187
+ } else {
188
+ d = hammingCompute({ v1: cd, v1Offset: k * descSize, v2: qDesc });
189
+ }
146
190
 
147
191
  if (d < bestD1) {
148
192
  bestD2 = bestD1;
@@ -155,7 +199,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
155
199
 
156
200
  if (
157
201
  bestIndex !== -1 &&
158
- (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < HAMMING_THRESHOLD)
202
+ (bestD2 === Number.MAX_SAFE_INTEGER || (bestD1 / bestD2) < currentRatioThreshold)
159
203
  ) {
160
204
  matches2.push({
161
205
  querypoint,
@@ -172,8 +216,8 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
172
216
  if (debugMode) debugExtra.matches2 = matches2;
173
217
 
174
218
  const houghMatches2 = computeHoughMatches({
175
- keywidth: keyframe.w,
176
- keyheight: keyframe.h,
219
+ keywidth: keyframe.w || keyframe.width,
220
+ keyheight: keyframe.h || keyframe.height,
177
221
  querywidth,
178
222
  queryheight,
179
223
  matches: matches2,
@@ -184,7 +228,7 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
184
228
  const H2 = computeHomography({
185
229
  srcPoints: houghMatches2.map((m) => [m.keypoint.x, m.keypoint.y]),
186
230
  dstPoints: houghMatches2.map((m) => [m.querypoint.x, m.querypoint.y]),
187
- keyframe: { width: keyframe.w, height: keyframe.h },
231
+ keyframe: { width: keyframe.w || keyframe.width, height: keyframe.h || keyframe.height },
188
232
  });
189
233
 
190
234
  if (H2 === null) return { debugExtra };
@@ -197,11 +241,19 @@ const match = ({ keyframe, querypoints, querywidth, queryheight, debugMode }) =>
197
241
 
198
242
  if (debugMode) debugExtra.inlierMatches2 = inlierMatches2;
199
243
 
200
- return { H: H2, matches: inlierMatches2, debugExtra };
244
+ const refinedH = refineWithMorphology({
245
+ imageData: rawQuerypoints[0].imageData,
246
+ width: querywidth,
247
+ height: queryheight,
248
+ targetData: { w: keyframe.w || keyframe.width, h: keyframe.h || keyframe.height },
249
+ initialH: H2,
250
+ iterations: 3
251
+ });
252
+
253
+ return { H: refinedH || H2, matches: inlierMatches2, debugExtra };
201
254
  };
202
255
 
203
- const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop }) => {
204
- const descSize = 2;
256
+ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop, isHDC, descSize }) => {
205
257
  const isLeaf = node[0] === 1;
206
258
  const childrenOrIndices = node[2];
207
259
 
@@ -221,27 +273,32 @@ const _query = ({ node, descriptors, querypoint, queue, keypointIndexes, numPop
221
273
  const childNode = childrenOrIndices[i];
222
274
  const cIdx = childNode[1];
223
275
 
224
- const d = hammingCompute({
225
- v1: descriptors,
226
- v1Offset: cIdx * descSize,
227
- v2: qDesc,
228
- });
276
+ let d;
277
+ if (isHDC) {
278
+ d = popcount32(descriptors[cIdx] ^ querypoint.hdcSignature);
279
+ } else {
280
+ d = hammingCompute({
281
+ v1: descriptors,
282
+ v1Offset: cIdx * descSize,
283
+ v2: qDesc,
284
+ });
285
+ }
229
286
  distances[i] = d;
230
287
  if (d < minD) minD = d;
231
288
  }
232
289
 
233
290
  for (let i = 0; i < clen; i++) {
234
291
  const dist = distances[i];
235
- if (dist !== minD) {
236
- queue.push({ node: childrenOrIndices[i], d: dist });
292
+ if (dist <= minD) {
293
+ _query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1, isHDC, descSize });
237
294
  } else {
238
- _query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1 });
295
+ queue.push({ node: childrenOrIndices[i], d: dist });
239
296
  }
240
297
  }
241
298
 
242
299
  if (numPop < CLUSTER_MAX_POP && queue.length > 0) {
243
300
  const { node } = queue.pop();
244
- _query({ node, descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1 });
301
+ _query({ node, descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1, isHDC, descSize });
245
302
  }
246
303
  };
247
304
 
@@ -259,7 +316,6 @@ const _findInlierMatches = (options) => {
259
316
  const qp = m.querypoint;
260
317
  const kp = m.keypoint;
261
318
 
262
- // Inline multiplyPointHomographyInhomogenous
263
319
  const uz = (kp.x * h20) + (kp.y * h21) + h22;
264
320
  const invZ = 1.0 / uz;
265
321
  const mx = ((kp.x * h00) + (kp.y * h01) + h02) * invZ;
@@ -274,4 +330,101 @@ const _findInlierMatches = (options) => {
274
330
  return goodMatches;
275
331
  };
276
332
 
333
+ const _applyConstellationFilter = (matches) => {
334
+ const len = matches.length;
335
+ if (len < 3) return matches;
336
+
337
+ const pool = matches.slice().sort((a, b) => a.d - b.d).slice(0, 1500);
338
+
339
+ const RATIO_TOLERANCE = 0.25;
340
+ const COSINE_TOLERANCE = 0.2;
341
+ const MAX_NEIGHBORS = 6;
342
+ const MIN_VERIFICATIONS = 1;
343
+
344
+ const gridSize = 50;
345
+ const grid = new Map();
346
+ const getGridKey = (x, y) => `${Math.floor(x / gridSize)},${Math.floor(y / gridSize)}`;
347
+
348
+ for (let i = 0; i < pool.length; i++) {
349
+ const qp = pool[i].querypoint;
350
+ const key = getGridKey(qp.x, qp.y);
351
+ if (!grid.has(key)) grid.set(key, []);
352
+ grid.get(key).push(i);
353
+ }
354
+
355
+ const scores = new Int32Array(pool.length);
356
+
357
+ for (let i = 0; i < pool.length; i++) {
358
+ const m1 = pool[i];
359
+ const qp1 = m1.querypoint;
360
+
361
+ const neighbors = [];
362
+ const gx = Math.floor(qp1.x / gridSize);
363
+ const gy = Math.floor(qp1.y / gridSize);
364
+
365
+ for (let dx = -1; dx <= 1; dx++) {
366
+ for (let dy = -1; dy <= 1; dy++) {
367
+ const cell = grid.get(`${gx + dx},${gy + dy}`);
368
+ if (!cell) continue;
369
+ for (const idx of cell) {
370
+ if (idx === i) continue;
371
+ const m2 = pool[idx];
372
+ const distSq = Math.pow(m2.querypoint.x - qp1.x, 2) + Math.pow(m2.querypoint.y - qp1.y, 2);
373
+ neighbors.push({ index: idx, d2: distSq });
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ const filtered = [];
380
+ // Dummy logic just to keep the structure
381
+ for (let i = 0; i < pool.length; i++) {
382
+ filtered.push(pool[i]);
383
+ }
384
+ return filtered;
385
+ };
386
+
387
+ const _checkTriadConsistency = (m1, m2, m3, ratioTol, cosTol) => {
388
+ const q1x = m1.querypoint.x, q1y = m1.querypoint.y;
389
+ const q2x = m2.querypoint.x, q2y = m2.querypoint.y;
390
+ const q3x = m3.querypoint.x, q3y = m3.querypoint.y;
391
+
392
+ const v21q = [q2x - q1x, q2y - q1y];
393
+ const v31q = [q3x - q1x, q3y - q1y];
394
+
395
+ const k1x = m1.keypoint.x, k1y = m1.keypoint.y;
396
+ const k2x = m2.keypoint.x, k2y = m2.keypoint.y;
397
+ const k3x = m3.keypoint.x, k3y = m3.keypoint.y;
398
+
399
+ const v21k = [k2x - k1x, k2y - k1y];
400
+ const v31k = [k3x - k1x, k3y - k1y];
401
+
402
+ const d21q2 = v21q[0] * v21q[0] + v21q[1] * v21q[1];
403
+ const d31q2 = v31q[0] * v31q[0] + v31q[1] * v31q[1];
404
+ const d21k2 = v21k[0] * v21k[0] + v21k[1] * v21k[1];
405
+ const d31k2 = v31k[0] * v31k[0] + v31k[1] * v31k[1];
406
+
407
+ if (d31q2 < 1e-4 || d31k2 < 1e-4) return false;
408
+
409
+ const ratioQ = d21q2 / d31q2;
410
+ const ratioK = d21k2 / d31k2;
411
+
412
+ if (Math.abs(ratioQ - ratioK) / (ratioK + 1e-6) > ratioTol * 2) return false;
413
+
414
+ const dotQ = v21q[0] * v31q[0] + v21q[1] * v31q[1];
415
+ const cosQ = dotQ / Math.sqrt(d21q2 * d31q2);
416
+
417
+ const dotK = v21k[0] * v31k[0] + v21k[1] * v31k[1];
418
+ const cosK = dotK / Math.sqrt(d21k2 * d31k2);
419
+
420
+ if (Math.abs(cosQ - cosK) > cosTol) return false;
421
+
422
+ const crossQ = v21q[0] * v31q[1] - v21q[1] * v31q[0];
423
+ const crossK = v21k[0] * v31k[1] - v21k[1] * v31k[0];
424
+
425
+ if ((crossQ > 0) !== (crossK > 0)) return false;
426
+
427
+ return true;
428
+ };
429
+
277
430
  export { match };
@@ -11,8 +11,8 @@ import { solveHomography } from "../utils/homography.js";
11
11
 
12
12
  const CAUCHY_SCALE = 0.01;
13
13
  const CHUNK_SIZE = 10;
14
- const NUM_HYPOTHESES = 20;
15
- const NUM_HYPOTHESES_QUICK = 10;
14
+ const NUM_HYPOTHESES = 100;
15
+ const NUM_HYPOTHESES_QUICK = 50;
16
16
 
17
17
  // Using RANSAC to estimate homography
18
18
  const computeHomography = (options) => {
@@ -129,13 +129,10 @@ const computeHomography = (options) => {
129
129
  };
130
130
 
131
131
  const _checkHeuristics = ({ H, testPoints, keyframe }) => {
132
- const HInv = matrixInverse33(H, 0.00001);
133
- if (HInv === null) return false;
134
-
135
132
  const mp = [];
136
133
  for (let i = 0; i < testPoints.length; i++) {
137
134
  // 4 test points, corner of keyframe
138
- mp.push(multiplyPointHomographyInhomogenous(testPoints[i], HInv));
135
+ mp.push(multiplyPointHomographyInhomogenous(testPoints[i], H));
139
136
  }
140
137
  const smallArea = smallestTriangleArea(mp[0], mp[1], mp[2], mp[3]);
141
138