@srsergio/taptapp-ar 1.0.89 โ 1.0.91
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 +8 -6
- 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/react/TaptappAR.js +7 -5
- package/dist/runtime/controller.js +19 -14
- package/dist/runtime/controller.worker.js +4 -1
- package/dist/runtime/simple-ar.js +2 -0
- 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/react/TaptappAR.tsx +7 -5
- package/src/runtime/controller.ts +20 -14
- package/src/runtime/controller.worker.js +4 -1
- package/src/runtime/simple-ar.ts +2 -0
package/README.md
CHANGED
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
- **16-bit Fourier Signatures**: Spatial ADN embedded in every feature for harmonic matching.
|
|
39
39
|
- **4-bit Packed Tracking Data**: Grayscale images are compressed to 4-bit depth, slashing file size.
|
|
40
40
|
- **64-bit LSH Descriptors**: Optimized Locality Sensitive Hashing for descriptors.
|
|
41
|
-
- ๐งต **High-Precision Tracking**: Now using **Float32** coordinate precision
|
|
41
|
+
- ๐งต **High-Precision Tracking**: Now using **Float32** coordinate precision with sub-pixel resolution and multi-octave verification (1%, 50%, 25%, 12.5% scales).
|
|
42
|
+
- ๐ **Ultra-Wide Scaling**: Enhanced Hough Transform supporting a massive scale range from **1% (distant targets)** to **1000% (extreme close-up)**.
|
|
43
|
+
- โก **Immediate AR Detection**: Optimized "warm-up" period (15 frames) with relaxed inlier thresholds (6 pts) for instant tracking lock.
|
|
42
44
|
- ๐ฆ **Framework Agnostic**: Includes wrappers for **A-Frame**, **Three.js**, and a raw **Controller** for custom engines.
|
|
43
45
|
- ๐ **Ultra-Compact Files**: Output `.taar` files are **~50KB** (vs ~380KB+ previously).
|
|
44
46
|
|
|
@@ -70,11 +72,11 @@ The latest version has been rigorously tested with an adaptive stress test (`rob
|
|
|
70
72
|
|
|
71
73
|
| Metric | Result | Description |
|
|
72
74
|
| :--- | :--- | :--- |
|
|
73
|
-
| **Pass Rate** | **
|
|
74
|
-
| **Drift Tolerance** | **<
|
|
75
|
-
| **Tracking Precision** | **
|
|
76
|
-
| **Detection Time** | **<
|
|
77
|
-
| **Total Pipeline** | **~
|
|
75
|
+
| **Pass Rate** | **96.8%** | High success rate across resolutions (209/216). |
|
|
76
|
+
| **Drift Tolerance** | **< 2%** | Validated via sub-pixel coordinate system restoration. |
|
|
77
|
+
| **Tracking Precision** | **Double-Precision Fix** | Corrected coordinate scaling for all image octaves. |
|
|
78
|
+
| **Detection Time** | **< 10ms** | Ultra-fast initial detection on standard CPU. |
|
|
79
|
+
| **Total Pipeline** | **~35ms** | Complete loop (Detect + Match + Track + Validate). |
|
|
78
80
|
|
|
79
81
|
---
|
|
80
82
|
|
|
@@ -11,6 +11,7 @@ import { DetectorLite } from "../core/detector/detector-lite.js";
|
|
|
11
11
|
import { build as hierarchicalClusteringBuild } from "../core/matching/hierarchical-clustering.js";
|
|
12
12
|
import * as protocol from "../core/protocol.js";
|
|
13
13
|
import { triangulate, getEdges } from "../core/utils/delaunay.js";
|
|
14
|
+
import { generateBasis, projectDescriptor, compressToSignature } from "../core/matching/hdc.js";
|
|
14
15
|
// Detect environment
|
|
15
16
|
const isNode = typeof process !== "undefined" &&
|
|
16
17
|
process.versions != null &&
|
|
@@ -72,12 +73,22 @@ export class OfflineCompiler {
|
|
|
72
73
|
const results = [];
|
|
73
74
|
for (let i = 0; i < targetImages.length; i++) {
|
|
74
75
|
const targetImage = targetImages[i];
|
|
75
|
-
const
|
|
76
|
+
const fullImageList = buildImageList(targetImage);
|
|
77
|
+
// ๐ MOONSHOT: Keep many scales for better robustness
|
|
78
|
+
const imageList = fullImageList;
|
|
76
79
|
const percentPerImageScale = percentPerImage / imageList.length;
|
|
77
80
|
const keyframes = [];
|
|
78
81
|
for (const image of imageList) {
|
|
79
|
-
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxFeaturesPerBucket:
|
|
82
|
+
const detector = new DetectorLite(image.width, image.height, { useLSH: true, maxFeaturesPerBucket: 40 });
|
|
80
83
|
const { featurePoints: ps } = detector.detect(image.data);
|
|
84
|
+
// HDC Pre-calculation
|
|
85
|
+
const hdcBasis = generateBasis(protocol.HDC_SEED, 1024);
|
|
86
|
+
for (const p of ps) {
|
|
87
|
+
if (p.descriptors) {
|
|
88
|
+
const hv = projectDescriptor(p.descriptors, hdcBasis);
|
|
89
|
+
p.hdcSignature = compressToSignature(hv);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
81
92
|
const maximaPoints = ps.filter((p) => p.maxima);
|
|
82
93
|
const minimaPoints = ps.filter((p) => !p.maxima);
|
|
83
94
|
const maximaPointsCluster = hierarchicalClusteringBuild({ points: maximaPoints });
|
|
@@ -168,8 +179,9 @@ export class OfflineCompiler {
|
|
|
168
179
|
w: kf.width,
|
|
169
180
|
h: kf.height,
|
|
170
181
|
s: kf.scale,
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
hdc: false,
|
|
183
|
+
max: protocol.columnarize(kf.maximaPoints, kf.maximaPointsCluster, kf.width, kf.height, false),
|
|
184
|
+
min: protocol.columnarize(kf.minimaPoints, kf.minimaPointsCluster, kf.width, kf.height, false),
|
|
173
185
|
})),
|
|
174
186
|
};
|
|
175
187
|
});
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
import { FREAKPOINTS } from "./freak.js";
|
|
13
13
|
import { gpuCompute } from "../utils/gpu-compute.js";
|
|
14
14
|
import { computeLSH64, computeFullFREAK, packLSHIntoDescriptor } from "../utils/lsh-direct.js";
|
|
15
|
+
import { generateBasis, projectDescriptor, compressToSignature } from "../matching/hdc.js";
|
|
16
|
+
import { HDC_SEED } from "../protocol.js";
|
|
15
17
|
const PYRAMID_MIN_SIZE = 4; // Restored to 4 for better small-scale detection
|
|
16
18
|
// PYRAMID_MAX_OCTAVE ya no es necesario, el lรญmite lo da PYRAMID_MIN_SIZE
|
|
17
|
-
const NUM_BUCKETS_PER_DIMENSION = 10
|
|
18
|
-
const DEFAULT_MAX_FEATURES_PER_BUCKET = 8
|
|
19
|
+
const NUM_BUCKETS_PER_DIMENSION = 15; // Increased from 10 to 15 for better local detail
|
|
20
|
+
const DEFAULT_MAX_FEATURES_PER_BUCKET = 12; // Increased from 8 to 12
|
|
19
21
|
const ORIENTATION_NUM_BINS = 36;
|
|
20
22
|
const FREAK_EXPANSION_FACTOR = 7.0;
|
|
21
23
|
// Global GPU mode flag
|
|
@@ -37,6 +39,7 @@ export class DetectorLite {
|
|
|
37
39
|
this.useGPU = options.useGPU !== undefined ? options.useGPU : globalUseGPU;
|
|
38
40
|
// Protocol V6 (Moonshot): 64-bit LSH is the standard descriptor format
|
|
39
41
|
this.useLSH = options.useLSH !== undefined ? options.useLSH : true;
|
|
42
|
+
this.useHDC = options.useHDC !== undefined ? options.useHDC : true; // Enabled by default for Moonshot
|
|
40
43
|
this.maxFeaturesPerBucket = options.maxFeaturesPerBucket !== undefined ? options.maxFeaturesPerBucket : DEFAULT_MAX_FEATURES_PER_BUCKET;
|
|
41
44
|
let numOctaves = 0;
|
|
42
45
|
let w = width, h = height;
|
|
@@ -79,6 +82,16 @@ export class DetectorLite {
|
|
|
79
82
|
this._computeOrientations(prunedExtremas, pyramidImages);
|
|
80
83
|
// 6. Calcular descriptores FREAK
|
|
81
84
|
this._computeFreakDescriptors(prunedExtremas, pyramidImages);
|
|
85
|
+
// 7. ๐ MOONSHOT: HDC Hyper-projection
|
|
86
|
+
if (this.useHDC) {
|
|
87
|
+
const hdcBasis = generateBasis(HDC_SEED, 1024);
|
|
88
|
+
for (const ext of prunedExtremas) {
|
|
89
|
+
if (ext.lsh) {
|
|
90
|
+
const hv = projectDescriptor(ext.lsh, hdcBasis);
|
|
91
|
+
ext.hdcSignature = compressToSignature(hv);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
// Convertir a formato de salida
|
|
83
96
|
const featurePoints = prunedExtremas.map(ext => {
|
|
84
97
|
const scale = Math.pow(2, ext.octave);
|
|
@@ -88,7 +101,9 @@ export class DetectorLite {
|
|
|
88
101
|
y: ext.y * scale + scale * 0.5 - 0.5,
|
|
89
102
|
scale: scale,
|
|
90
103
|
angle: ext.angle || 0,
|
|
91
|
-
descriptors: (this.useLSH && ext.lsh) ? ext.lsh : (ext.descriptors || [])
|
|
104
|
+
descriptors: (this.useLSH && ext.lsh) ? ext.lsh : (ext.descriptors || []),
|
|
105
|
+
hdcSignature: ext.hdcSignature || 0,
|
|
106
|
+
imageData: data // Pass source image for refinement
|
|
92
107
|
};
|
|
93
108
|
});
|
|
94
109
|
return { featurePoints, pyramid: pyramidImages };
|
|
@@ -154,35 +169,36 @@ export class DetectorLite {
|
|
|
154
169
|
// Horizontal pass - Speed optimized with manual border handling
|
|
155
170
|
for (let y = 0; y < height; y++) {
|
|
156
171
|
const rowOffset = y * width;
|
|
157
|
-
// Left border
|
|
158
|
-
|
|
159
|
-
temp[rowOffset
|
|
172
|
+
// Left border (Normalized)
|
|
173
|
+
const sumL0 = k0 + k1 + k2 + k1 + k0; // Ideal sum
|
|
174
|
+
temp[rowOffset] = (data[rowOffset] * (k0 + k1 + k2) + data[rowOffset + 1] * k1 + data[rowOffset + 2] * k0) * (1.0 / (k0 + k1 + k2));
|
|
175
|
+
temp[rowOffset + 1] = (data[rowOffset] * k1 + data[rowOffset + 1] * k2 + data[rowOffset + 2] * k1 + data[rowOffset + 3] * k0) * (1.0 / (k1 + k2 + k1 + k0));
|
|
160
176
|
// Main loop - NO boundary checks
|
|
161
177
|
for (let x = 2; x < width - 2; x++) {
|
|
162
178
|
const pos = rowOffset + x;
|
|
163
179
|
temp[pos] = data[pos - 2] * k0 + data[pos - 1] * k1 + data[pos] * k2 + data[pos + 1] * k1 + data[pos + 2] * k0;
|
|
164
180
|
}
|
|
165
|
-
// Right border
|
|
181
|
+
// Right border (Normalized)
|
|
166
182
|
const r2 = rowOffset + width - 2;
|
|
167
183
|
const r1 = rowOffset + width - 1;
|
|
168
|
-
temp[r2] = data[r2 - 2] * k0 + data[r2 - 1] * k1 + data[r2] * k2 + data[r1] * k1;
|
|
169
|
-
temp[r1] = data[r1 - 2] * k0 + data[r1 - 1] * k1 + data[r1] * (k2 + k1 + k0);
|
|
184
|
+
temp[r2] = (data[r2 - 2] * k0 + data[r2 - 1] * k1 + data[r2] * k2 + data[r1] * k1) * (1.0 / (k0 + k1 + k2 + k1));
|
|
185
|
+
temp[r1] = (data[r1 - 2] * k0 + data[r1 - 1] * k1 + data[r1] * (k2 + k1 + k0)) * (1.0 / (k0 + k1 + k2));
|
|
170
186
|
}
|
|
171
187
|
// Vertical pass - Speed optimized
|
|
172
188
|
for (let x = 0; x < width; x++) {
|
|
173
|
-
// Top border
|
|
174
|
-
output[x] = temp[x] * (k0 + k1 + k2) + temp[x + width] * k1 + temp[x + width * 2] * k0;
|
|
175
|
-
output[x + width] = temp[x] * k1 + temp[x + width] * k2 + temp[x + width * 2] * k1 + temp[x + width * 3] * k0;
|
|
189
|
+
// Top border (Normalized)
|
|
190
|
+
output[x] = (temp[x] * (k0 + k1 + k2) + temp[x + width] * k1 + temp[x + width * 2] * k0) * (1.0 / (k0 + k1 + k2));
|
|
191
|
+
output[x + width] = (temp[x] * k1 + temp[x + width] * k2 + temp[x + width * 2] * k1 + temp[x + width * 3] * k0) * (1.0 / (k1 + k2 + k1 + k0));
|
|
176
192
|
// Main loop - NO boundary checks
|
|
177
193
|
for (let y = 2; y < height - 2; y++) {
|
|
178
194
|
const p = y * width + x;
|
|
179
195
|
output[p] = temp[p - width * 2] * k0 + temp[p - width] * k1 + temp[p] * k2 + temp[p + width] * k1 + temp[p + width * 2] * k0;
|
|
180
196
|
}
|
|
181
|
-
// Bottom border
|
|
197
|
+
// Bottom border (Normalized)
|
|
182
198
|
const b2 = (height - 2) * width + x;
|
|
183
199
|
const b1 = (height - 1) * width + x;
|
|
184
|
-
output[b2] = temp[b2 - width * 2] * k0 + temp[b2 - width] * k1 + temp[b2] * k2 + temp[b1] * k1;
|
|
185
|
-
output[b1] = temp[b1 - width * 2] * k0 + temp[b1 - width] * k1 + temp[b1] * (k2 + k1 + k0);
|
|
200
|
+
output[b2] = (temp[b2 - width * 2] * k0 + temp[b2 - width] * k1 + temp[b2] * k2 + temp[b1] * k1) * (1.0 / (k0 + k1 + k2 + k1));
|
|
201
|
+
output[b1] = (temp[b1 - width * 2] * k0 + temp[b1 - width] * k1 + temp[b1] * (k2 + k1 + k0)) * (1.0 / (k0 + k1 + k2));
|
|
186
202
|
}
|
|
187
203
|
return { data: output, width, height };
|
|
188
204
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ MOONSHOT: Direct PnP Solver for AR
|
|
3
|
+
*
|
|
4
|
+
* Instead of estimating a 2D Homography and decomposing it,
|
|
5
|
+
* we solve for the 3D Pose [R|t] directly using the
|
|
6
|
+
* Perspective-n-Point algorithm.
|
|
7
|
+
*/
|
|
1
8
|
export function estimate({ screenCoords, worldCoords, projectionTransform }: {
|
|
2
9
|
screenCoords: any;
|
|
3
10
|
worldCoords: any;
|
|
@@ -1,51 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// Ref: can anyone provide reference?
|
|
1
|
+
import { solvePosePnP } from "./pnp-solver.js";
|
|
2
|
+
/**
|
|
3
|
+
* ๐ MOONSHOT: Direct PnP Solver for AR
|
|
4
|
+
*
|
|
5
|
+
* Instead of estimating a 2D Homography and decomposing it,
|
|
6
|
+
* we solve for the 3D Pose [R|t] directly using the
|
|
7
|
+
* Perspective-n-Point algorithm.
|
|
8
|
+
*/
|
|
10
9
|
const estimate = ({ screenCoords, worldCoords, projectionTransform }) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
]);
|
|
17
|
-
const K = new Matrix(projectionTransform);
|
|
18
|
-
const KInv = inverse(K);
|
|
19
|
-
const _KInvH = KInv.mmul(H);
|
|
20
|
-
const KInvH = _KInvH.to1DArray();
|
|
21
|
-
const norm1 = Math.sqrt(KInvH[0] * KInvH[0] + KInvH[3] * KInvH[3] + KInvH[6] * KInvH[6]);
|
|
22
|
-
const norm2 = Math.sqrt(KInvH[1] * KInvH[1] + KInvH[4] * KInvH[4] + KInvH[7] * KInvH[7]);
|
|
23
|
-
const tnorm = (norm1 + norm2) / 2;
|
|
24
|
-
const rotate = [];
|
|
25
|
-
rotate[0] = KInvH[0] / norm1;
|
|
26
|
-
rotate[3] = KInvH[3] / norm1;
|
|
27
|
-
rotate[6] = KInvH[6] / norm1;
|
|
28
|
-
rotate[1] = KInvH[1] / norm2;
|
|
29
|
-
rotate[4] = KInvH[4] / norm2;
|
|
30
|
-
rotate[7] = KInvH[7] / norm2;
|
|
31
|
-
rotate[2] = rotate[3] * rotate[7] - rotate[6] * rotate[4];
|
|
32
|
-
rotate[5] = rotate[6] * rotate[1] - rotate[0] * rotate[7];
|
|
33
|
-
rotate[8] = rotate[0] * rotate[4] - rotate[1] * rotate[3];
|
|
34
|
-
const norm3 = Math.sqrt(rotate[2] * rotate[2] + rotate[5] * rotate[5] + rotate[8] * rotate[8]);
|
|
35
|
-
rotate[2] /= norm3;
|
|
36
|
-
rotate[5] /= norm3;
|
|
37
|
-
rotate[8] /= norm3;
|
|
38
|
-
// TODO: artoolkit has check_rotation() that somehow switch the rotate vector. not sure what that does. Can anyone advice?
|
|
39
|
-
// https://github.com/artoolkitx/artoolkit5/blob/5bf0b671ff16ead527b9b892e6aeb1a2771f97be/lib/SRC/ARICP/icpUtil.c#L215
|
|
40
|
-
const tran = [];
|
|
41
|
-
tran[0] = KInvH[2] / tnorm;
|
|
42
|
-
tran[1] = KInvH[5] / tnorm;
|
|
43
|
-
tran[2] = KInvH[8] / tnorm;
|
|
44
|
-
let initialModelViewTransform = [
|
|
45
|
-
[rotate[0], rotate[1], rotate[2], tran[0]],
|
|
46
|
-
[rotate[3], rotate[4], rotate[5], tran[1]],
|
|
47
|
-
[rotate[6], rotate[7], rotate[8], tran[2]],
|
|
48
|
-
];
|
|
49
|
-
return initialModelViewTransform;
|
|
10
|
+
return solvePosePnP({
|
|
11
|
+
screenCoords,
|
|
12
|
+
worldCoords,
|
|
13
|
+
projectionTransform
|
|
14
|
+
});
|
|
50
15
|
};
|
|
51
16
|
export { estimate };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Morphological Refinement - "Active Edge Alignment"
|
|
3
|
+
*
|
|
4
|
+
* This Moonshot algorithm snaps the projected target to the real image edges.
|
|
5
|
+
* It solves the "Small Box / Drift" problem by maximizing alignment with
|
|
6
|
+
* local image gradients using a Spring-Mass optimization system.
|
|
7
|
+
*/
|
|
8
|
+
import { Matrix, SingularValueDecomposition } from "ml-matrix";
|
|
9
|
+
export function refineWithMorphology({ imageData, width, height, targetData, initialH, iterations = 3 }) {
|
|
10
|
+
let currentH = [...initialH];
|
|
11
|
+
// 1. Boundary Points (The "Anchors" of our elastic malla)
|
|
12
|
+
const boundaryPoints = [];
|
|
13
|
+
const step = 0.05;
|
|
14
|
+
for (let i = 0; i <= 1.0; i += step) {
|
|
15
|
+
boundaryPoints.push({ x: i * targetData.w, y: 0 });
|
|
16
|
+
boundaryPoints.push({ x: i * targetData.w, y: targetData.h });
|
|
17
|
+
boundaryPoints.push({ x: 0, y: i * targetData.h });
|
|
18
|
+
boundaryPoints.push({ x: targetData.w, y: i * targetData.h });
|
|
19
|
+
}
|
|
20
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
21
|
+
const correspondences = [];
|
|
22
|
+
for (const pt of boundaryPoints) {
|
|
23
|
+
// Project
|
|
24
|
+
const w = currentH[6] * pt.x + currentH[7] * pt.y + currentH[8];
|
|
25
|
+
const sx = (currentH[0] * pt.x + currentH[1] * pt.y + currentH[2]) / w;
|
|
26
|
+
const sy = (currentH[3] * pt.x + currentH[4] * pt.y + currentH[5]) / w;
|
|
27
|
+
if (sx < 2 || sx >= width - 2 || sy < 2 || sy >= height - 2)
|
|
28
|
+
continue;
|
|
29
|
+
// 2. Local Gradient Search (The "Pull" of the image)
|
|
30
|
+
const searchDist = 10;
|
|
31
|
+
let bestX = sx;
|
|
32
|
+
let bestY = sy;
|
|
33
|
+
let maxGrad = -1;
|
|
34
|
+
for (let dy = -searchDist; dy <= searchDist; dy += 2) {
|
|
35
|
+
for (let dx = -searchDist; dx <= searchDist; dx += 2) {
|
|
36
|
+
const nx = Math.floor(sx + dx);
|
|
37
|
+
const ny = Math.floor(sy + dy);
|
|
38
|
+
const idx = ny * width + nx;
|
|
39
|
+
// Sobel-like gradient magnitude
|
|
40
|
+
const gx = imageData[idx + 1] - imageData[idx - 1];
|
|
41
|
+
const gy = imageData[idx + width] - imageData[idx - width];
|
|
42
|
+
const grad = gx * gx + gy * gy;
|
|
43
|
+
if (grad > maxGrad) {
|
|
44
|
+
maxGrad = grad;
|
|
45
|
+
bestX = nx;
|
|
46
|
+
bestY = ny;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (maxGrad > 500) {
|
|
51
|
+
correspondences.push({
|
|
52
|
+
src: pt,
|
|
53
|
+
dst: { x: bestX, y: bestY },
|
|
54
|
+
weight: Math.min(1.0, maxGrad / 15000)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (correspondences.length < 10)
|
|
59
|
+
break;
|
|
60
|
+
// 3. Solve for best Homography using SVD
|
|
61
|
+
const nextH = solveDLTWeight(correspondences);
|
|
62
|
+
if (nextH) {
|
|
63
|
+
// Soft-Update (Momentum)
|
|
64
|
+
for (let i = 0; i < 9; i++) {
|
|
65
|
+
currentH[i] = currentH[i] * 0.5 + nextH[i] * 0.5;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return currentH;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Direct Linear Transform with Weights
|
|
73
|
+
*/
|
|
74
|
+
function solveDLTWeight(pairs) {
|
|
75
|
+
const n = pairs.length;
|
|
76
|
+
const A = new Matrix(n * 2, 9);
|
|
77
|
+
for (let i = 0; i < n; i++) {
|
|
78
|
+
const { src, dst, weight: w } = pairs[i];
|
|
79
|
+
const x = src.x;
|
|
80
|
+
const y = src.y;
|
|
81
|
+
const xp = dst.x;
|
|
82
|
+
const yp = dst.y;
|
|
83
|
+
// Row 1
|
|
84
|
+
A.set(i * 2, 0, 0);
|
|
85
|
+
A.set(i * 2, 1, 0);
|
|
86
|
+
A.set(i * 2, 2, 0);
|
|
87
|
+
A.set(i * 2, 3, -x * w);
|
|
88
|
+
A.set(i * 2, 4, -y * w);
|
|
89
|
+
A.set(i * 2, 5, -w);
|
|
90
|
+
A.set(i * 2, 6, yp * x * w);
|
|
91
|
+
A.set(i * 2, 7, yp * y * w);
|
|
92
|
+
A.set(i * 2, 8, yp * w);
|
|
93
|
+
// Row 2
|
|
94
|
+
A.set(i * 2 + 1, 0, x * w);
|
|
95
|
+
A.set(i * 2 + 1, 1, y * w);
|
|
96
|
+
A.set(i * 2 + 1, 2, w);
|
|
97
|
+
A.set(i * 2 + 1, 3, 0);
|
|
98
|
+
A.set(i * 2 + 1, 4, 0);
|
|
99
|
+
A.set(i * 2 + 1, 5, 0);
|
|
100
|
+
A.set(i * 2 + 1, 6, -xp * x * w);
|
|
101
|
+
A.set(i * 2 + 1, 7, -xp * y * w);
|
|
102
|
+
A.set(i * 2 + 1, 8, -xp * w);
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const svd = new SingularValueDecomposition(A);
|
|
106
|
+
const V = svd.rightSingularVectors;
|
|
107
|
+
// Last column of V is the solution
|
|
108
|
+
const h = V.getColumn(8);
|
|
109
|
+
// Normalize H[8] to 1
|
|
110
|
+
const scale = 1.0 / h[8];
|
|
111
|
+
return h.map(v => v * scale);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct PnP (Perspective-n-Point) Solver for Planar Targets
|
|
3
|
+
*
|
|
4
|
+
* This Moonshot algorithm ignores octave-relative scales and works
|
|
5
|
+
* purely in Physical World Units. It uses the Camera Matrix (K)
|
|
6
|
+
* to deduce the real-world distance (Z).
|
|
7
|
+
*/
|
|
8
|
+
import { Matrix, SingularValueDecomposition } from "ml-matrix";
|
|
9
|
+
export function solvePosePnP({ screenCoords, worldCoords, projectionTransform }) {
|
|
10
|
+
const K = new Matrix(projectionTransform);
|
|
11
|
+
const n = screenCoords.length;
|
|
12
|
+
// 1. Build the DLT matrix for Pose (Directly estimating [R|t])
|
|
13
|
+
// We assume worldCoords are [X, Y, 0]
|
|
14
|
+
// Eq: x = K * [R|t] * X
|
|
15
|
+
// K^-1 * x = [r1 r2 t] * [X Y 1]^T
|
|
16
|
+
const KI = Inverse3x3(projectionTransform);
|
|
17
|
+
const A = new Matrix(n * 2, 9);
|
|
18
|
+
for (let i = 0; i < n; i++) {
|
|
19
|
+
const sci = screenCoords[i];
|
|
20
|
+
const wci = worldCoords[i];
|
|
21
|
+
// Normalized camera coordinates
|
|
22
|
+
const nx = KI[0] * sci.x + KI[1] * sci.y + KI[2];
|
|
23
|
+
const ny = KI[3] * sci.x + KI[4] * sci.y + KI[5];
|
|
24
|
+
const nz = KI[6] * sci.x + KI[7] * sci.y + KI[8];
|
|
25
|
+
const unx = nx / nz;
|
|
26
|
+
const uny = ny / nz;
|
|
27
|
+
// DLT equations for [r11 r12 r21 r22 r31 r32 t1 t2 t3]
|
|
28
|
+
const X = wci.x;
|
|
29
|
+
const Y = wci.y;
|
|
30
|
+
// Row 1: X*r11 + Y*r12 + t1 - unx*(X*r31 + Y*r32 + t3) = 0
|
|
31
|
+
A.set(i * 2, 0, X);
|
|
32
|
+
A.set(i * 2, 1, Y);
|
|
33
|
+
A.set(i * 2, 2, 1);
|
|
34
|
+
A.set(i * 2, 3, 0);
|
|
35
|
+
A.set(i * 2, 4, 0);
|
|
36
|
+
A.set(i * 2, 5, 0);
|
|
37
|
+
A.set(i * 2, 6, -unx * X);
|
|
38
|
+
A.set(i * 2, 7, -unx * Y);
|
|
39
|
+
A.set(i * 2, 8, -unx);
|
|
40
|
+
// Row 2: X*r21 + Y*r22 + t2 - uny*(X*r31 + Y*r32 + t3) = 0
|
|
41
|
+
A.set(i * 2 + 1, 0, 0);
|
|
42
|
+
A.set(i * 2 + 1, 1, 0);
|
|
43
|
+
A.set(i * 2 + 1, 2, 0);
|
|
44
|
+
A.set(i * 2 + 1, 3, X);
|
|
45
|
+
A.set(i * 2 + 1, 4, Y);
|
|
46
|
+
A.set(i * 2 + 1, 5, 1);
|
|
47
|
+
A.set(i * 2 + 1, 6, -uny * X);
|
|
48
|
+
A.set(i * 2 + 1, 7, -uny * Y);
|
|
49
|
+
A.set(i * 2 + 1, 8, -uny);
|
|
50
|
+
}
|
|
51
|
+
// Solve via SVD
|
|
52
|
+
const svd = new SingularValueDecomposition(A);
|
|
53
|
+
const V = svd.rightSingularVectors;
|
|
54
|
+
const sol = V.getColumn(8); // last column
|
|
55
|
+
// 3. Extract r1, r2 and t from the DLT solution
|
|
56
|
+
// Standard DLT has an overall sign ambiguity. We force sol[8] (t3) to be positive.
|
|
57
|
+
if (sol[8] < 0) {
|
|
58
|
+
for (let i = 0; i < 9; i++)
|
|
59
|
+
sol[i] = -sol[i];
|
|
60
|
+
}
|
|
61
|
+
const r1_raw = [sol[0], sol[3], sol[6]];
|
|
62
|
+
const r2_raw = [sol[1], sol[4], sol[7]];
|
|
63
|
+
const t_raw = [sol[2], sol[5], sol[8]];
|
|
64
|
+
const scale1 = Math.sqrt(r1_raw[0] ** 2 + r1_raw[1] ** 2 + r1_raw[2] ** 2);
|
|
65
|
+
const scale2 = Math.sqrt(r2_raw[0] ** 2 + r2_raw[1] ** 2 + r2_raw[2] ** 2);
|
|
66
|
+
const scale = (scale1 + scale2) / 2;
|
|
67
|
+
// 4. Construct Rotation Matrix and orthogonalize via SVD
|
|
68
|
+
const R_approx = new Matrix([
|
|
69
|
+
[r1_raw[0] / scale1, r2_raw[0] / scale2, 0],
|
|
70
|
+
[r1_raw[1] / scale1, r2_raw[1] / scale2, 0],
|
|
71
|
+
[r1_raw[2] / scale1, r2_raw[2] / scale2, 0]
|
|
72
|
+
]);
|
|
73
|
+
// R3 = R1 x R2
|
|
74
|
+
R_approx.set(0, 2, R_approx.get(1, 0) * R_approx.get(2, 1) - R_approx.get(2, 0) * R_approx.get(1, 1));
|
|
75
|
+
R_approx.set(1, 2, R_approx.get(2, 0) * R_approx.get(0, 1) - R_approx.get(0, 0) * R_approx.get(2, 1));
|
|
76
|
+
R_approx.set(2, 2, R_approx.get(0, 0) * R_approx.get(1, 1) - R_approx.get(1, 0) * R_approx.get(0, 1));
|
|
77
|
+
const svdRot = new SingularValueDecomposition(R_approx);
|
|
78
|
+
const U = svdRot.leftSingularVectors;
|
|
79
|
+
const Vrot = svdRot.rightSingularVectors;
|
|
80
|
+
let R = U.mmul(Vrot.transpose());
|
|
81
|
+
const getDet3 = (m) => {
|
|
82
|
+
return m.get(0, 0) * (m.get(1, 1) * m.get(2, 2) - m.get(1, 2) * m.get(2, 1)) -
|
|
83
|
+
m.get(0, 1) * (m.get(1, 0) * m.get(2, 2) - m.get(1, 2) * m.get(2, 0)) +
|
|
84
|
+
m.get(0, 2) * (m.get(1, 0) * m.get(2, 1) - m.get(1, 1) * m.get(2, 0));
|
|
85
|
+
};
|
|
86
|
+
if (getDet3(R) < 0) {
|
|
87
|
+
const U_mat = U.clone();
|
|
88
|
+
for (let i = 0; i < 3; i++)
|
|
89
|
+
U_mat.set(i, 2, -U_mat.get(i, 2));
|
|
90
|
+
R = U_mat.mmul(Vrot.transpose());
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
[R.get(0, 0), R.get(0, 1), R.get(0, 2), t_raw[0] / scale],
|
|
94
|
+
[R.get(1, 0), R.get(1, 1), R.get(1, 2), t_raw[1] / scale],
|
|
95
|
+
[R.get(2, 0), R.get(2, 1), R.get(2, 2), t_raw[2] / scale]
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
function Inverse3x3(m) {
|
|
99
|
+
const k00 = m[0][0], k01 = m[0][1], k02 = m[0][2];
|
|
100
|
+
const k10 = m[1][0], k11 = m[1][1], k12 = m[1][2];
|
|
101
|
+
const k20 = m[2][0], k21 = m[2][1], k22 = m[2][2];
|
|
102
|
+
const det = k00 * (k11 * k22 - k21 * k12) - k01 * (k10 * k22 - k12 * k20) + k02 * (k10 * k21 - k11 * k20);
|
|
103
|
+
const invDet = 1.0 / det;
|
|
104
|
+
return [
|
|
105
|
+
(k11 * k22 - k12 * k21) * invDet, (k02 * k21 - k01 * k22) * invDet, (k01 * k12 - k02 * k11) * invDet,
|
|
106
|
+
(k12 * k20 - k10 * k22) * invDet, (k00 * k22 - k02 * k20) * invDet, (k10 * k02 - k00 * k12) * invDet,
|
|
107
|
+
(k10 * k21 - k11 * k20) * invDet, (k20 * k01 - k21 * k00) * invDet, (k00 * k11 - k10 * k01) * invDet
|
|
108
|
+
];
|
|
109
|
+
}
|
|
@@ -32,15 +32,32 @@ class InputLoader {
|
|
|
32
32
|
if (this.context) {
|
|
33
33
|
this.context.clearRect(0, 0, this.width, this.height);
|
|
34
34
|
const isInputRotated = input.width === this.height && input.height === this.width;
|
|
35
|
+
const inputW = isInputRotated ? input.height : input.width;
|
|
36
|
+
const inputH = isInputRotated ? input.width : input.height;
|
|
37
|
+
const inputAspect = inputW / inputH;
|
|
38
|
+
const canvasAspect = this.width / this.height;
|
|
39
|
+
let sx = 0, sy = 0, sw = inputW, sh = inputH;
|
|
40
|
+
if (inputAspect > canvasAspect) {
|
|
41
|
+
// Input is wider than canvas - crop sides
|
|
42
|
+
sw = inputH * canvasAspect;
|
|
43
|
+
sx = (inputW - sw) / 2;
|
|
44
|
+
}
|
|
45
|
+
else if (inputAspect < canvasAspect) {
|
|
46
|
+
// Input is taller than canvas - crop top/bottom
|
|
47
|
+
sh = inputW / canvasAspect;
|
|
48
|
+
sy = (inputH - sh) / 2;
|
|
49
|
+
}
|
|
35
50
|
if (isInputRotated) {
|
|
36
51
|
this.context.save();
|
|
37
52
|
this.context.translate(this.width / 2, this.height / 2);
|
|
38
53
|
this.context.rotate(Math.PI / 2);
|
|
39
|
-
|
|
54
|
+
// Map source crop (relative to rotated input)
|
|
55
|
+
// Since input is already rotated, we crop based on the rotated dimensions
|
|
56
|
+
this.context.drawImage(input, sx, sy, sw, sh, -this.height / 2, -this.width / 2, this.height, this.width);
|
|
40
57
|
this.context.restore();
|
|
41
58
|
}
|
|
42
59
|
else {
|
|
43
|
-
this.context.drawImage(input, 0, 0, this.width, this.height);
|
|
60
|
+
this.context.drawImage(input, sx, sy, sw, sh, 0, 0, this.width, this.height);
|
|
44
61
|
}
|
|
45
62
|
const imageData = this.context.getImageData(0, 0, this.width, this.height);
|
|
46
63
|
this._convertToGrayscale(imageData.data, this.width, this.height);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperdimensional Computing (HDC) Core for AR
|
|
3
|
+
*
|
|
4
|
+
* Provides ultra-fast, ultra-compressed feature matching using
|
|
5
|
+
* High-Dimensional Random Vectors.
|
|
6
|
+
*/
|
|
7
|
+
export declare const HDC_DIMENSION = 1024;
|
|
8
|
+
export declare const HDC_WORDS: number;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a deterministic basis of Hypervectors
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateBasis(seed: number, count: number): Uint32Array[];
|
|
13
|
+
/**
|
|
14
|
+
* Projects a 64-bit descriptor into the Hyperdimensional Space
|
|
15
|
+
* Uses "Random Projection" logic (Locality Sensitive Hashing in HDC)
|
|
16
|
+
*/
|
|
17
|
+
export declare function projectDescriptor(desc: Uint32Array, basis: Uint32Array[]): Uint32Array;
|
|
18
|
+
/**
|
|
19
|
+
* Compresses an HDC vector into an "Ultra-Short Signature" (32 bits)
|
|
20
|
+
* This allows storing 1000 points in just 4KB of descriptors.
|
|
21
|
+
*/
|
|
22
|
+
export declare function compressToSignature(hv: Uint32Array): number;
|
|
23
|
+
/**
|
|
24
|
+
* Bundles multiple points into a single Global Hypervector (The "Image DNA")
|
|
25
|
+
* This allows checking if an image is present with ONE vector comparison.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bundle(hvs: Uint32Array[]): Uint32Array;
|