@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
|
@@ -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;
|
|
5
|
-
const NUM_ASSIGNMENT_HYPOTHESES = 12;
|
|
4
|
+
const MIN_FEATURE_PER_NODE = 32;
|
|
5
|
+
const NUM_ASSIGNMENT_HYPOTHESES = 12;
|
|
6
6
|
const NUM_CENTERS = 8;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 =
|
|
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(
|
|
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 >=
|
|
165
|
+
if (distBinX >= relaxedDelta) continue;
|
|
165
166
|
|
|
166
167
|
const distBinY = Math.abs(queryBins.binY - (binY + 0.5));
|
|
167
|
-
if (distBinY >=
|
|
168
|
+
if (distBinY >= relaxedDelta) continue;
|
|
168
169
|
|
|
169
170
|
const distBinScale = Math.abs(queryBins.binScale - (binScale + 0.5));
|
|
170
|
-
if (distBinScale >=
|
|
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 >=
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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:
|
|
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) /
|
|
51
|
-
y: (keypoint.y + 0.5) /
|
|
61
|
+
x: (keypoint.x + 0.5) / scale,
|
|
62
|
+
y: (keypoint.y + 0.5) / scale,
|
|
52
63
|
z: 0,
|
|
53
64
|
});
|
|
54
65
|
}
|
|
55
|
-
return {
|
|
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 =
|
|
8
|
-
const MIN_NUM_INLIERS =
|
|
9
|
-
const CLUSTER_MAX_POP =
|
|
10
|
-
const HAMMING_THRESHOLD = 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
|
|
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
|
-
|
|
52
|
-
|
|
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) <
|
|
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
|
-
|
|
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)
|
|
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;
|
|
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
|
-
|
|
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) <
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
236
|
-
|
|
292
|
+
if (dist <= minD) {
|
|
293
|
+
_query({ node: childrenOrIndices[i], descriptors, querypoint, queue, keypointIndexes, numPop: numPop + 1, isHDC, descSize });
|
|
237
294
|
} else {
|
|
238
|
-
|
|
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 =
|
|
15
|
-
const NUM_HYPOTHESES_QUICK =
|
|
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],
|
|
135
|
+
mp.push(multiplyPointHomographyInhomogenous(testPoints[i], H));
|
|
139
136
|
}
|
|
140
137
|
const smallArea = smallestTriangleArea(mp[0], mp[1], mp[2], mp[3]);
|
|
141
138
|
|