@srsergio/taptapp-ar 1.0.87 → 1.0.89

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 CHANGED
@@ -29,10 +29,12 @@
29
29
 
30
30
  ## 🌟 Key Features
31
31
 
32
- - 🖼️ **Hyper-Fast Compiler**: Pure JavaScript compiler that generates `.taar` files in **< 3s**.
32
+ - 🎭 **Non-Rigid Surface Tracking**: Supports curved and deformable surfaces using **Delaunay Meshes** and **Mass-Spring Relaxation**.
33
+ - 🚀 **Hyper-Fast Compiler**: Pure JavaScript compiler that generates `.taar` files in **< 3s**.
33
34
  - ⚡ **No TensorFlow Dependency**: No TFJS at all. Works natively in any JS environment (Node, Browser, Workers).
34
35
  - 🧬 **Fourier Positional Encoding**: Uses high-frequency sine/cosine mappings (GPT-style) for neural-like spatial consistency.
35
36
  - 🚀 **Protocol V7 (Moonshot)**:
37
+ - **Delaunay Triangular Grid**: Adaptive mesh that tracks surface deformations.
36
38
  - **16-bit Fourier Signatures**: Spatial ADN embedded in every feature for harmonic matching.
37
39
  - **4-bit Packed Tracking Data**: Grayscale images are compressed to 4-bit depth, slashing file size.
38
40
  - **64-bit LSH Descriptors**: Optimized Locality Sensitive Hashing for descriptors.
@@ -54,9 +56,9 @@ npm install @srsergio/taptapp-ar
54
56
 
55
57
  | Metric | Official MindAR | TapTapp AR V7 | Improvement |
56
58
  | :--- | :--- | :--- | :--- |
57
- | **Compilation Time** | ~23.50s | **~2.61s** | 🚀 **~9x Faster** |
58
- | **Output Size (.taar)** | ~770 KB | **~50 KB** | 📉 **93% Smaller** |
59
- | **Descriptor Format** | 84-byte Float | **64-bit LSH** | 🧠 **Massive Data Saving** |
59
+ | **Compilation Time** | ~23.50s | **~0.93s** | 🚀 **~25x Faster** |
60
+ | **Output Size (.taar)** | ~770 KB | **~338 KB** | 📉 **56% Smaller** |
61
+ | **Descriptor Format** | 84-byte Float | **128-bit LSH** | 🧠 **Massive Data Saving** |
60
62
  | **Tracking Data** | 8-bit Gray | **4-bit Packed** | 📦 **50% Data Saving** |
61
63
  | **Dependency Size** | ~20MB (TFJS) | **< 100KB** | 📦 **99% Smaller Bundle** |
62
64
 
@@ -68,11 +70,11 @@ The latest version has been rigorously tested with an adaptive stress test (`rob
68
70
 
69
71
  | Metric | Result | Description |
70
72
  | :--- | :--- | :--- |
71
- | **Pass Rate** | **96.3%** | High success rate across resolutions. |
72
- | **Drift Tolerance** | **< 15%** | Validated geometrically against ground truth metadata. |
73
+ | **Pass Rate** | **93.5%** | High success rate across resolutions (202/216). |
74
+ | **Drift Tolerance** | **< 10%** | Validated geometrically against ground truth metadata. |
73
75
  | **Tracking Precision** | **Float32** | Full 32-bit precision for optical flow tracking. |
74
- | **Detection Time** | **~21ms** | Ultra-fast initial detection on standard CPU. |
75
- | **Total Pipeline** | **~64ms** | Complete loop (Detect + Match + Track + Validate). |
76
+ | **Detection Time** | **< 20ms** | Ultra-fast initial detection on standard CPU. |
77
+ | **Total Pipeline** | **~45ms** | Complete loop (Detect + Match + Track + Validate). |
76
78
 
77
79
  ---
78
80
 
@@ -258,6 +260,8 @@ ar.stop();
258
260
  ## 🏗️ Protocol V7 (Moonshot Packed Format)
259
261
  TapTapp AR uses a proprietary **Moonshot Vision Codec** that is significantly more efficient than standard AR formats.
260
262
 
263
+ - **Non-Rigid Surface Tracking**: Replaces the standard rigid homography with a dynamic **Delaunay Mesh**. This allows the tracker to follow the curvature of posters on cylinders, t-shirts, or slightly bent magazines.
264
+ - **Mass-Spring Relaxation**: The tracking mesh is optimized using physical relaxation, minimizing L2 distance between predicted and tracked points while maintaining topological rigidity.
261
265
  - **Fourier Positional Encoding**: Maps 2D coordinates into a 16-dimensional frequency space. This creates a "Neural Consistency Check" that filters out noise and motion blur by checking for harmonic spatial agreement.
262
266
  - **4-bit Packed Tracking Data**: Image data used for optical flow is compressed to 4-bit depth.
263
267
  - **64-bit LSH Fingerprinting**: Feature descriptors are compressed to just 8 bytes using LSH.
@@ -271,3 +275,4 @@ TapTapp AR uses a proprietary **Moonshot Vision Codec** that is significantly mo
271
275
  MIT © [srsergiolazaro](https://github.com/srsergiolazaro)
272
276
 
273
277
  Based on the core research of MindAR, but completely re-written for high-performance binary processing and JS-only execution.
278
+
@@ -10,6 +10,7 @@ import { extractTrackingFeatures } from "../core/tracker/extract-utils.js";
10
10
  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
+ import { triangulate, getEdges } from "../core/utils/delaunay.js";
13
14
  // Detect environment
14
15
  const isNode = typeof process !== "undefined" &&
15
16
  process.versions != null &&
@@ -141,6 +142,14 @@ export class OfflineCompiler {
141
142
  px[i] = td.points[i].x;
142
143
  py[i] = td.points[i].y;
143
144
  }
145
+ const triangles = triangulate(td.points);
146
+ const edges = getEdges(triangles);
147
+ const restLengths = new Float32Array(edges.length);
148
+ for (let j = 0; j < edges.length; j++) {
149
+ const p1 = td.points[edges[j][0]];
150
+ const p2 = td.points[edges[j][1]];
151
+ restLengths[j] = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
152
+ }
144
153
  return {
145
154
  w: td.width,
146
155
  h: td.height,
@@ -148,6 +157,11 @@ export class OfflineCompiler {
148
157
  px,
149
158
  py,
150
159
  d: td.data,
160
+ mesh: {
161
+ t: new Uint16Array(triangles.flat()),
162
+ e: new Uint16Array(edges.flat()),
163
+ rl: restLengths
164
+ }
151
165
  };
152
166
  }),
153
167
  matchingData: item.matchingData.map((kf) => ({
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 🚀 Moonshot: Non-Rigid Surface Refinement (Mass-Spring System)
3
+ *
4
+ * Instead of a single homography, we relax a triangle mesh to match tracked points
5
+ * while preserving edge lengths (Isometric constraint).
6
+ */
7
+ export function refineNonRigid({ mesh, trackedPoints, currentVertices, iterations }: {
8
+ mesh: any;
9
+ trackedPoints: any;
10
+ currentVertices: any;
11
+ iterations?: number | undefined;
12
+ }): Float32Array<any>;
13
+ /**
14
+ * Maps a mesh from reference space to screen space using a homography
15
+ */
16
+ export function projectMesh(mesh: any, homography: any, width: any, height: any): Float32Array<ArrayBuffer>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 🚀 Moonshot: Non-Rigid Surface Refinement (Mass-Spring System)
3
+ *
4
+ * Instead of a single homography, we relax a triangle mesh to match tracked points
5
+ * while preserving edge lengths (Isometric constraint).
6
+ */
7
+ export function refineNonRigid({ mesh, trackedPoints, currentVertices, iterations = 5 }) {
8
+ const { e: edges, rl: restLengths } = mesh;
9
+ const numVertices = currentVertices.length / 2;
10
+ const vertices = new Float32Array(currentVertices); // copy
11
+ // lambda for stiffness
12
+ const stiffness = 0.8;
13
+ const dataFidelity = 0.5;
14
+ for (let iter = 0; iter < iterations; iter++) {
15
+ // 1. Edge Length Constraints (Isometric term)
16
+ for (let i = 0; i < restLengths.length; i++) {
17
+ const idx1 = edges[i * 2];
18
+ const idx2 = edges[i * 2 + 1];
19
+ const restL = restLengths[i];
20
+ const vx1 = vertices[idx1 * 2];
21
+ const vy1 = vertices[idx1 * 2 + 1];
22
+ const vx2 = vertices[idx2 * 2];
23
+ const vy2 = vertices[idx2 * 2 + 1];
24
+ const dx = vx2 - vx1;
25
+ const dy = vy2 - vy1;
26
+ const currentL = Math.sqrt(dx * dx + dy * dy);
27
+ if (currentL < 0.0001)
28
+ continue;
29
+ const diff = (currentL - restL) / currentL;
30
+ const moveX = dx * 0.5 * diff * stiffness;
31
+ const moveY = dy * 0.5 * diff * stiffness;
32
+ vertices[idx1 * 2] += moveX;
33
+ vertices[idx1 * 2 + 1] += moveY;
34
+ vertices[idx2 * 2] -= moveX;
35
+ vertices[idx2 * 2 + 1] -= moveY;
36
+ }
37
+ // 2. Data Fidelity Constraints (Alignment with NCC tracker)
38
+ for (const tp of trackedPoints) {
39
+ const idx = tp.meshIndex;
40
+ if (idx === undefined)
41
+ continue;
42
+ const targetX = tp.x;
43
+ const targetY = tp.y;
44
+ vertices[idx * 2] += (targetX - vertices[idx * 2]) * dataFidelity;
45
+ vertices[idx * 2 + 1] += (targetY - vertices[idx * 2 + 1]) * dataFidelity;
46
+ }
47
+ }
48
+ return vertices;
49
+ }
50
+ /**
51
+ * Maps a mesh from reference space to screen space using a homography
52
+ */
53
+ export function projectMesh(mesh, homography, width, height) {
54
+ const { px, py } = mesh; // original octave points used as mesh vertices
55
+ const numVertices = px.length;
56
+ const projected = new Float32Array(numVertices * 2);
57
+ const h = homography;
58
+ const h00 = h[0][0], h01 = h[0][1], h02 = h[0][3];
59
+ const h10 = h[1][0], h11 = h[1][1], h12 = h[1][3];
60
+ const h20 = h[2][0], h21 = h[2][1], h22 = h[2][3];
61
+ for (let i = 0; i < numVertices; i++) {
62
+ const x = px[i];
63
+ const y = py[i];
64
+ const uz = (x * h20) + (y * h21) + h22;
65
+ const invZ = 1.0 / uz;
66
+ projected[i * 2] = ((x * h00) + (y * h01) + h02) * invZ;
67
+ projected[i * 2 + 1] = ((x * h10) + (y * h11) + h12) * invZ;
68
+ }
69
+ return projected;
70
+ }
@@ -1,4 +1,4 @@
1
- export declare const CURRENT_VERSION = 7;
1
+ export declare const CURRENT_VERSION = 8;
2
2
  /**
3
3
  * Morton Order calculation for spatial sorting
4
4
  */
@@ -1,5 +1,5 @@
1
1
  import * as msgpack from "@msgpack/msgpack";
2
- export const CURRENT_VERSION = 7;
2
+ export const CURRENT_VERSION = 8;
3
3
  /**
4
4
  * Morton Order calculation for spatial sorting
5
5
  */
@@ -137,6 +137,11 @@ export function decodeTaar(buffer) {
137
137
  if (td.d)
138
138
  td.d = unpacked;
139
139
  }
140
+ if (td.mesh) {
141
+ td.mesh.t = normalizeBuffer(td.mesh.t, Uint16Array);
142
+ td.mesh.e = normalizeBuffer(td.mesh.e, Uint16Array);
143
+ td.mesh.rl = normalizeBuffer(td.mesh.rl, Float32Array);
144
+ }
140
145
  }
141
146
  // 2. Process Matching Data
142
147
  for (const kf of item.matchingData) {
@@ -8,6 +8,7 @@ export class Tracker {
8
8
  debugMode: boolean;
9
9
  trackingKeyframeList: any[];
10
10
  prebuiltData: any[];
11
+ meshVerticesState: any[];
11
12
  templateBuffer: Float32Array<ArrayBuffer>;
12
13
  dummyRun(inputData: any): void;
13
14
  track(inputData: any, lastModelViewTransform: any, targetIndex: any): {
@@ -17,6 +18,7 @@ export class Tracker {
17
18
  debugExtra: {};
18
19
  indices?: undefined;
19
20
  octaveIndex?: undefined;
21
+ deformedMesh?: undefined;
20
22
  } | {
21
23
  worldCoords: {
22
24
  x: number;
@@ -30,6 +32,10 @@ export class Tracker {
30
32
  reliabilities: number[];
31
33
  indices: number[];
32
34
  octaveIndex: any;
35
+ deformedMesh: {
36
+ vertices: Float32Array<ArrayBuffer>;
37
+ triangles: any;
38
+ } | null;
33
39
  debugExtra: {};
34
40
  };
35
41
  lastOctaveIndex: any[] | undefined;
@@ -1,4 +1,5 @@
1
1
  import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
2
+ import { refineNonRigid, projectMesh } from "../estimation/non-rigid-refine.js";
2
3
  const AR2_DEFAULT_TS = 6;
3
4
  const AR2_DEFAULT_TS_GAP = 1;
4
5
  const AR2_SEARCH_SIZE = 12; // Reduced from 25 to 12 for high-speed tracking (25 is overkill)
@@ -25,10 +26,13 @@ class Tracker {
25
26
  width: keyframe.w,
26
27
  height: keyframe.h,
27
28
  scale: keyframe.s,
29
+ mesh: keyframe.mesh,
28
30
  // Recyclable projected image buffer
29
31
  projectedImage: new Float32Array(keyframe.w * keyframe.h)
30
32
  }));
31
33
  }
34
+ // Maintain mesh vertices state for temporal continuity
35
+ this.meshVerticesState = []; // [targetIndex][octaveIndex]
32
36
  // Pre-allocate template data buffer to avoid garbage collection
33
37
  const templateOneSize = AR2_DEFAULT_TS;
34
38
  const templateSize = templateOneSize * 2 + 1;
@@ -84,7 +88,8 @@ class Tracker {
84
88
  const { px, py, s: scale } = trackingFrame;
85
89
  const reliabilities = [];
86
90
  for (let i = 0; i < matchingPoints.length; i++) {
87
- if (sim[i] > AR2_SIM_THRESH && i < px.length) {
91
+ const reliability = sim[i];
92
+ if (reliability > AR2_SIM_THRESH && i < px.length) {
88
93
  goodTrack.push(i);
89
94
  const point = computeScreenCoordiate(modelViewProjectionTransform, matchingPoints[i][0], matchingPoints[i][1]);
90
95
  screenCoords.push(point);
@@ -93,9 +98,53 @@ class Tracker {
93
98
  y: py[i] / scale,
94
99
  z: 0,
95
100
  });
96
- reliabilities.push(sim[i]);
101
+ reliabilities.push(reliability);
97
102
  }
98
103
  }
104
+ // --- 🚀 MOONSHOT: Non-Rigid Mesh Refinement ---
105
+ let deformedMesh = null;
106
+ if (prebuilt.mesh && goodTrack.length >= 4) {
107
+ if (!this.meshVerticesState[targetIndex])
108
+ this.meshVerticesState[targetIndex] = [];
109
+ let currentOctaveVertices = this.meshVerticesState[targetIndex][octaveIndex];
110
+ // Initial setup: If no state, use the reference points (normalized) as first guess
111
+ if (!currentOctaveVertices) {
112
+ currentOctaveVertices = new Float32Array(px.length * 2);
113
+ for (let i = 0; i < px.length; i++) {
114
+ currentOctaveVertices[i * 2] = px[i];
115
+ currentOctaveVertices[i * 2 + 1] = py[i];
116
+ }
117
+ }
118
+ // Data fidelity: Prepare tracked targets for mass-spring
119
+ const trackedTargets = [];
120
+ for (let j = 0; j < goodTrack.length; j++) {
121
+ const idx = goodTrack[j];
122
+ trackedTargets.push({
123
+ meshIndex: idx,
124
+ x: matchingPoints[idx][0] * scale, // Convert back to octave space pixels
125
+ y: matchingPoints[idx][1] * scale
126
+ });
127
+ }
128
+ // Relax mesh in octave space
129
+ const refinedOctaveVertices = refineNonRigid({
130
+ mesh: prebuilt.mesh,
131
+ trackedPoints: trackedTargets,
132
+ currentVertices: currentOctaveVertices,
133
+ iterations: 5
134
+ });
135
+ this.meshVerticesState[targetIndex][octaveIndex] = refinedOctaveVertices;
136
+ // Project deformed mesh to screen space
137
+ const screenMeshVertices = new Float32Array(refinedOctaveVertices.length);
138
+ for (let i = 0; i < refinedOctaveVertices.length; i += 2) {
139
+ const p = computeScreenCoordiate(modelViewProjectionTransform, refinedOctaveVertices[i] / scale, refinedOctaveVertices[i + 1] / scale);
140
+ screenMeshVertices[i] = p.x;
141
+ screenMeshVertices[i + 1] = p.y;
142
+ }
143
+ deformedMesh = {
144
+ vertices: screenMeshVertices,
145
+ triangles: prebuilt.mesh.t
146
+ };
147
+ }
99
148
  // 2.1 Spatial distribution check: Avoid getting stuck in corners/noise
100
149
  if (screenCoords.length >= 8) {
101
150
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
@@ -125,7 +174,7 @@ class Tracker {
125
174
  trackedPoints: screenCoords,
126
175
  };
127
176
  }
128
- return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, debugExtra };
177
+ return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, deformedMesh, debugExtra };
129
178
  }
130
179
  /**
131
180
  * Pure JS implementation of NCC matching
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 🚀 Moonshot: Simple incremental Delaunay Triangulation (Bowyer-Watson)
3
+ *
4
+ * Used to create a topological mesh covering the image target features.
5
+ */
6
+ export function triangulate(points: any): number[][];
7
+ /**
8
+ * Extract edges from triangles for Mass-Spring logic
9
+ */
10
+ export function getEdges(triangles: any): number[][];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * 🚀 Moonshot: Simple incremental Delaunay Triangulation (Bowyer-Watson)
3
+ *
4
+ * Used to create a topological mesh covering the image target features.
5
+ */
6
+ export function triangulate(points) {
7
+ if (points.length < 3)
8
+ return [];
9
+ // 1. Create a super-triangle that contains all points
10
+ // Find min/max bounds
11
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
12
+ for (const p of points) {
13
+ if (p.x < minX)
14
+ minX = p.x;
15
+ if (p.x > maxX)
16
+ maxX = p.x;
17
+ if (p.y < minY)
18
+ minY = p.y;
19
+ if (p.y > maxY)
20
+ maxY = p.y;
21
+ }
22
+ const dx = maxX - minX;
23
+ const dy = maxY - minY;
24
+ const deltaMax = Math.max(dx, dy);
25
+ const midX = (minX + maxX) / 2;
26
+ const midY = (minY + maxY) / 2;
27
+ // A triangle large enough to cover all points
28
+ const p1 = { x: midX - 20 * deltaMax, y: midY - deltaMax };
29
+ const p2 = { x: midX, y: midY + 20 * deltaMax };
30
+ const p3 = { x: midX + 20 * deltaMax, y: midY - deltaMax };
31
+ let triangles = [
32
+ { p1, p2, p3, indices: [-1, -2, -3] }
33
+ ];
34
+ // 2. Add points one by one
35
+ for (let i = 0; i < points.length; i++) {
36
+ const p = points[i];
37
+ const badTriangles = [];
38
+ for (const t of triangles) {
39
+ if (isInCircumcircle(p, t)) {
40
+ badTriangles.push(t);
41
+ }
42
+ }
43
+ const polygon = [];
44
+ for (const t of badTriangles) {
45
+ const edges = [
46
+ { a: t.p1, b: t.p2, i1: t.indices[0], i2: t.indices[1] },
47
+ { a: t.p2, b: t.p3, i1: t.indices[1], i2: t.indices[2] },
48
+ { a: t.p3, b: t.p1, i1: t.indices[2], i2: t.indices[0] }
49
+ ];
50
+ for (const edge of edges) {
51
+ let isShared = false;
52
+ for (const t2 of badTriangles) {
53
+ if (t === t2)
54
+ continue;
55
+ if (isSameEdge(edge, t2)) {
56
+ isShared = true;
57
+ break;
58
+ }
59
+ }
60
+ if (!isShared) {
61
+ polygon.push(edge);
62
+ }
63
+ }
64
+ }
65
+ // Remove bad triangles
66
+ triangles = triangles.filter(t => !badTriangles.includes(t));
67
+ // Add new triangles from polygon edges to point p
68
+ for (const edge of polygon) {
69
+ triangles.push({
70
+ p1: edge.a,
71
+ p2: edge.b,
72
+ p3: p,
73
+ indices: [edge.i1, edge.i2, i]
74
+ });
75
+ }
76
+ }
77
+ // 3. Remove triangles that share vertices with the super-triangle
78
+ return triangles.filter(t => {
79
+ return t.indices[0] >= 0 && t.indices[1] >= 0 && t.indices[2] >= 0;
80
+ }).map(t => t.indices);
81
+ }
82
+ function isInCircumcircle(p, t) {
83
+ const x1 = t.p1.x, y1 = t.p1.y;
84
+ const x2 = t.p2.x, y2 = t.p2.y;
85
+ const x3 = t.p3.x, y3 = t.p3.y;
86
+ const D = 2 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
87
+ const centerX = ((x1 * x1 + y1 * y1) * (y2 - y3) + (x2 * x2 + y2 * y2) * (y3 - y1) + (x3 * x3 + y3 * y3) * (y1 - y2)) / D;
88
+ const centerY = ((x1 * x1 + y1 * y1) * (x3 - x2) + (x2 * x2 + y2 * y2) * (x1 - x3) + (x3 * x3 + y3 * y3) * (x2 - x1)) / D;
89
+ const radiusSq = (x1 - centerX) * (x1 - centerX) + (y1 - centerY) * (y1 - centerY);
90
+ const distSq = (p.x - centerX) * (p.x - centerX) + (p.y - centerY) * (p.y - centerY);
91
+ return distSq <= radiusSq;
92
+ }
93
+ function isSameEdge(edge, triangle) {
94
+ const tEdges = [
95
+ [triangle.indices[0], triangle.indices[1]],
96
+ [triangle.indices[1], triangle.indices[2]],
97
+ [triangle.indices[2], triangle.indices[0]]
98
+ ];
99
+ for (const te of tEdges) {
100
+ if ((edge.i1 === te[0] && edge.i2 === te[1]) || (edge.i1 === te[1] && edge.i2 === te[0])) {
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * Extract edges from triangles for Mass-Spring logic
108
+ */
109
+ export function getEdges(triangles) {
110
+ const edgeSet = new Set();
111
+ const edges = [];
112
+ for (const t of triangles) {
113
+ const pairs = [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]];
114
+ for (const pair of pairs) {
115
+ const low = Math.min(pair[0], pair[1]);
116
+ const high = Math.max(pair[0], pair[1]);
117
+ const key = `${low}-${high}`;
118
+ if (!edgeSet.has(key)) {
119
+ edgeSet.add(key);
120
+ edges.push([low, high]);
121
+ }
122
+ }
123
+ }
124
+ return edges;
125
+ }
@@ -68,10 +68,17 @@ declare class Controller {
68
68
  featurePoints: any;
69
69
  }>;
70
70
  _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number): Promise<{
71
+ modelViewTransform: null;
72
+ screenCoords: any[];
73
+ reliabilities: number[];
74
+ stabilities: number[];
75
+ deformedMesh?: undefined;
76
+ } | {
71
77
  modelViewTransform: any;
72
78
  screenCoords: any[];
73
79
  reliabilities: number[];
74
80
  stabilities: number[];
81
+ deformedMesh: any;
75
82
  }>;
76
83
  processVideo(input: any): void;
77
84
  stopProcessVideo(): void;
@@ -93,6 +100,7 @@ declare class Controller {
93
100
  debugExtra: {};
94
101
  indices?: undefined;
95
102
  octaveIndex?: undefined;
103
+ deformedMesh?: undefined;
96
104
  } | {
97
105
  worldCoords: {
98
106
  x: number;
@@ -106,6 +114,10 @@ declare class Controller {
106
114
  reliabilities: number[];
107
115
  indices: number[];
108
116
  octaveIndex: any;
117
+ deformedMesh: {
118
+ vertices: Float32Array<ArrayBuffer>;
119
+ triangles: any;
120
+ } | null;
109
121
  debugExtra: {};
110
122
  }>;
111
123
  trackUpdate(modelViewTransform: number[][], trackFeatures: any): Promise<any>;
@@ -193,9 +193,9 @@ class Controller {
193
193
  return { targetIndex, modelViewTransform, screenCoords, worldCoords, featurePoints };
194
194
  }
195
195
  async _trackAndUpdate(inputData, lastModelViewTransform, targetIndex) {
196
- const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0 } = await this._workerTrack(inputData, lastModelViewTransform, targetIndex);
196
+ const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0, deformedMesh } = await this._workerTrack(inputData, lastModelViewTransform, targetIndex);
197
197
  if (!worldCoords || worldCoords.length === 0) {
198
- return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
198
+ return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [], deformedMesh: null };
199
199
  }
200
200
  const state = this.trackingStates[targetIndex];
201
201
  if (!state.pointStabilities)
@@ -264,13 +264,15 @@ class Controller {
264
264
  stabilities: finalWorldCoords.map((_, i) => {
265
265
  const globalIdx = indices[i];
266
266
  return stabilities[globalIdx];
267
- })
267
+ }),
268
+ deformedMesh
268
269
  });
269
270
  return {
270
271
  modelViewTransform,
271
272
  screenCoords: finalScreenCoords,
272
273
  reliabilities: finalReliabilities,
273
- stabilities: finalStabilities
274
+ stabilities: finalStabilities,
275
+ deformedMesh
274
276
  };
275
277
  }
276
278
  processVideo(input) {
@@ -329,6 +331,7 @@ class Controller {
329
331
  trackingState.screenCoords = result.screenCoords;
330
332
  trackingState.reliabilities = result.reliabilities;
331
333
  trackingState.stabilities = result.stabilities;
334
+ trackingState.deformedMesh = result.deformedMesh;
332
335
  }
333
336
  }
334
337
  const wasShowing = trackingState.showing;
@@ -365,7 +368,8 @@ class Controller {
365
368
  modelViewTransform: trackingState.currentModelViewTransform,
366
369
  screenCoords: trackingState.screenCoords,
367
370
  reliabilities: trackingState.reliabilities,
368
- stabilities: trackingState.stabilities
371
+ stabilities: trackingState.stabilities,
372
+ deformedMesh: trackingState.deformedMesh
369
373
  });
370
374
  }
371
375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@srsergio/taptapp-ar",
3
- "version": "1.0.87",
3
+ "version": "1.0.89",
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",
@@ -11,6 +11,7 @@ import { extractTrackingFeatures } from "../core/tracker/extract-utils.js";
11
11
  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
+ import { triangulate, getEdges } from "../core/utils/delaunay.js";
14
15
 
15
16
  // Detect environment
16
17
  const isNode = typeof process !== "undefined" &&
@@ -176,6 +177,15 @@ export class OfflineCompiler {
176
177
  px[i] = td.points[i].x;
177
178
  py[i] = td.points[i].y;
178
179
  }
180
+ const triangles = triangulate(td.points);
181
+ const edges = getEdges(triangles);
182
+ const restLengths = new Float32Array(edges.length);
183
+ for (let j = 0; j < edges.length; j++) {
184
+ const p1 = td.points[edges[j][0]];
185
+ const p2 = td.points[edges[j][1]];
186
+ restLengths[j] = Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
187
+ }
188
+
179
189
  return {
180
190
  w: td.width,
181
191
  h: td.height,
@@ -183,6 +193,11 @@ export class OfflineCompiler {
183
193
  px,
184
194
  py,
185
195
  d: td.data,
196
+ mesh: {
197
+ t: new Uint16Array(triangles.flat()),
198
+ e: new Uint16Array(edges.flat()),
199
+ rl: restLengths
200
+ }
186
201
  };
187
202
  }),
188
203
  matchingData: item.matchingData.map((kf: any) => ({
@@ -0,0 +1,85 @@
1
+ /**
2
+ * 🚀 Moonshot: Non-Rigid Surface Refinement (Mass-Spring System)
3
+ *
4
+ * Instead of a single homography, we relax a triangle mesh to match tracked points
5
+ * while preserving edge lengths (Isometric constraint).
6
+ */
7
+
8
+ export function refineNonRigid({ mesh, trackedPoints, currentVertices, iterations = 5 }) {
9
+ const { e: edges, rl: restLengths } = mesh;
10
+ const numVertices = currentVertices.length / 2;
11
+ const vertices = new Float32Array(currentVertices); // copy
12
+
13
+ // lambda for stiffness
14
+ const stiffness = 0.8;
15
+ const dataFidelity = 0.5;
16
+
17
+ for (let iter = 0; iter < iterations; iter++) {
18
+ // 1. Edge Length Constraints (Isometric term)
19
+ for (let i = 0; i < restLengths.length; i++) {
20
+ const idx1 = edges[i * 2];
21
+ const idx2 = edges[i * 2 + 1];
22
+ const restL = restLengths[i];
23
+
24
+ const vx1 = vertices[idx1 * 2];
25
+ const vy1 = vertices[idx1 * 2 + 1];
26
+ const vx2 = vertices[idx2 * 2];
27
+ const vy2 = vertices[idx2 * 2 + 1];
28
+
29
+ const dx = vx2 - vx1;
30
+ const dy = vy2 - vy1;
31
+ const currentL = Math.sqrt(dx * dx + dy * dy);
32
+
33
+ if (currentL < 0.0001) continue;
34
+
35
+ const diff = (currentL - restL) / currentL;
36
+ const moveX = dx * 0.5 * diff * stiffness;
37
+ const moveY = dy * 0.5 * diff * stiffness;
38
+
39
+ vertices[idx1 * 2] += moveX;
40
+ vertices[idx1 * 2 + 1] += moveY;
41
+ vertices[idx2 * 2] -= moveX;
42
+ vertices[idx2 * 2 + 1] -= moveY;
43
+ }
44
+
45
+ // 2. Data Fidelity Constraints (Alignment with NCC tracker)
46
+ for (const tp of trackedPoints) {
47
+ const idx = tp.meshIndex;
48
+ if (idx === undefined) continue;
49
+
50
+ const targetX = tp.x;
51
+ const targetY = tp.y;
52
+
53
+ vertices[idx * 2] += (targetX - vertices[idx * 2]) * dataFidelity;
54
+ vertices[idx * 2 + 1] += (targetY - vertices[idx * 2 + 1]) * dataFidelity;
55
+ }
56
+ }
57
+
58
+ return vertices;
59
+ }
60
+
61
+ /**
62
+ * Maps a mesh from reference space to screen space using a homography
63
+ */
64
+ export function projectMesh(mesh, homography, width, height) {
65
+ const { px, py } = mesh; // original octave points used as mesh vertices
66
+ const numVertices = px.length;
67
+ const projected = new Float32Array(numVertices * 2);
68
+
69
+ const h = homography;
70
+ const h00 = h[0][0], h01 = h[0][1], h02 = h[0][3];
71
+ const h10 = h[1][0], h11 = h[1][1], h12 = h[1][3];
72
+ const h20 = h[2][0], h21 = h[2][1], h22 = h[2][3];
73
+
74
+ for (let i = 0; i < numVertices; i++) {
75
+ const x = px[i];
76
+ const y = py[i];
77
+
78
+ const uz = (x * h20) + (y * h21) + h22;
79
+ const invZ = 1.0 / uz;
80
+ projected[i * 2] = ((x * h00) + (y * h01) + h02) * invZ;
81
+ projected[i * 2 + 1] = ((x * h10) + (y * h11) + h12) * invZ;
82
+ }
83
+
84
+ return projected;
85
+ }
@@ -1,6 +1,6 @@
1
1
  import * as msgpack from "@msgpack/msgpack";
2
2
 
3
- export const CURRENT_VERSION = 7;
3
+ export const CURRENT_VERSION = 8;
4
4
 
5
5
  /**
6
6
  * Morton Order calculation for spatial sorting
@@ -157,6 +157,12 @@ export function decodeTaar(buffer: ArrayBuffer | Uint8Array) {
157
157
  if (td.data) td.data = unpacked;
158
158
  if (td.d) td.d = unpacked;
159
159
  }
160
+
161
+ if (td.mesh) {
162
+ td.mesh.t = normalizeBuffer(td.mesh.t, Uint16Array);
163
+ td.mesh.e = normalizeBuffer(td.mesh.e, Uint16Array);
164
+ td.mesh.rl = normalizeBuffer(td.mesh.rl, Float32Array);
165
+ }
160
166
  }
161
167
 
162
168
  // 2. Process Matching Data
@@ -1,4 +1,5 @@
1
1
  import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
2
+ import { refineNonRigid, projectMesh } from "../estimation/non-rigid-refine.js";
2
3
 
3
4
  const AR2_DEFAULT_TS = 6;
4
5
  const AR2_DEFAULT_TS_GAP = 1;
@@ -37,11 +38,15 @@ class Tracker {
37
38
  width: keyframe.w,
38
39
  height: keyframe.h,
39
40
  scale: keyframe.s,
41
+ mesh: keyframe.mesh,
40
42
  // Recyclable projected image buffer
41
43
  projectedImage: new Float32Array(keyframe.w * keyframe.h)
42
44
  }));
43
45
  }
44
46
 
47
+ // Maintain mesh vertices state for temporal continuity
48
+ this.meshVerticesState = []; // [targetIndex][octaveIndex]
49
+
45
50
  // Pre-allocate template data buffer to avoid garbage collection
46
51
  const templateOneSize = AR2_DEFAULT_TS;
47
52
  const templateSize = templateOneSize * 2 + 1;
@@ -122,7 +127,8 @@ class Tracker {
122
127
  const reliabilities = [];
123
128
 
124
129
  for (let i = 0; i < matchingPoints.length; i++) {
125
- if (sim[i] > AR2_SIM_THRESH && i < px.length) {
130
+ const reliability = sim[i];
131
+ if (reliability > AR2_SIM_THRESH && i < px.length) {
126
132
  goodTrack.push(i);
127
133
  const point = computeScreenCoordiate(
128
134
  modelViewProjectionTransform,
@@ -135,8 +141,63 @@ class Tracker {
135
141
  y: py[i] / scale,
136
142
  z: 0,
137
143
  });
138
- reliabilities.push(sim[i]);
144
+ reliabilities.push(reliability);
145
+ }
146
+ }
147
+
148
+ // --- 🚀 MOONSHOT: Non-Rigid Mesh Refinement ---
149
+ let deformedMesh = null;
150
+ if (prebuilt.mesh && goodTrack.length >= 4) {
151
+ if (!this.meshVerticesState[targetIndex]) this.meshVerticesState[targetIndex] = [];
152
+
153
+ let currentOctaveVertices = this.meshVerticesState[targetIndex][octaveIndex];
154
+
155
+ // Initial setup: If no state, use the reference points (normalized) as first guess
156
+ if (!currentOctaveVertices) {
157
+ currentOctaveVertices = new Float32Array(px.length * 2);
158
+ for (let i = 0; i < px.length; i++) {
159
+ currentOctaveVertices[i * 2] = px[i];
160
+ currentOctaveVertices[i * 2 + 1] = py[i];
161
+ }
162
+ }
163
+
164
+ // Data fidelity: Prepare tracked targets for mass-spring
165
+ const trackedTargets = [];
166
+ for (let j = 0; j < goodTrack.length; j++) {
167
+ const idx = goodTrack[j];
168
+ trackedTargets.push({
169
+ meshIndex: idx,
170
+ x: matchingPoints[idx][0] * scale, // Convert back to octave space pixels
171
+ y: matchingPoints[idx][1] * scale
172
+ });
139
173
  }
174
+
175
+ // Relax mesh in octave space
176
+ const refinedOctaveVertices = refineNonRigid({
177
+ mesh: prebuilt.mesh,
178
+ trackedPoints: trackedTargets,
179
+ currentVertices: currentOctaveVertices,
180
+ iterations: 5
181
+ });
182
+
183
+ this.meshVerticesState[targetIndex][octaveIndex] = refinedOctaveVertices;
184
+
185
+ // Project deformed mesh to screen space
186
+ const screenMeshVertices = new Float32Array(refinedOctaveVertices.length);
187
+ for (let i = 0; i < refinedOctaveVertices.length; i += 2) {
188
+ const p = computeScreenCoordiate(
189
+ modelViewProjectionTransform,
190
+ refinedOctaveVertices[i] / scale,
191
+ refinedOctaveVertices[i + 1] / scale
192
+ );
193
+ screenMeshVertices[i] = p.x;
194
+ screenMeshVertices[i + 1] = p.y;
195
+ }
196
+
197
+ deformedMesh = {
198
+ vertices: screenMeshVertices,
199
+ triangles: prebuilt.mesh.t
200
+ };
140
201
  }
141
202
 
142
203
  // 2.1 Spatial distribution check: Avoid getting stuck in corners/noise
@@ -164,7 +225,7 @@ class Tracker {
164
225
  };
165
226
  }
166
227
 
167
- return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, debugExtra };
228
+ return { worldCoords, screenCoords, reliabilities, indices: goodTrack, octaveIndex, deformedMesh, debugExtra };
168
229
  }
169
230
 
170
231
  /**
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 🚀 Moonshot: Simple incremental Delaunay Triangulation (Bowyer-Watson)
3
+ *
4
+ * Used to create a topological mesh covering the image target features.
5
+ */
6
+
7
+ export function triangulate(points) {
8
+ if (points.length < 3) return [];
9
+
10
+ // 1. Create a super-triangle that contains all points
11
+ // Find min/max bounds
12
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
13
+ for (const p of points) {
14
+ if (p.x < minX) minX = p.x;
15
+ if (p.x > maxX) maxX = p.x;
16
+ if (p.y < minY) minY = p.y;
17
+ if (p.y > maxY) maxY = p.y;
18
+ }
19
+
20
+ const dx = maxX - minX;
21
+ const dy = maxY - minY;
22
+ const deltaMax = Math.max(dx, dy);
23
+ const midX = (minX + maxX) / 2;
24
+ const midY = (minY + maxY) / 2;
25
+
26
+ // A triangle large enough to cover all points
27
+ const p1 = { x: midX - 20 * deltaMax, y: midY - deltaMax };
28
+ const p2 = { x: midX, y: midY + 20 * deltaMax };
29
+ const p3 = { x: midX + 20 * deltaMax, y: midY - deltaMax };
30
+
31
+ let triangles = [
32
+ { p1, p2, p3, indices: [-1, -2, -3] }
33
+ ];
34
+
35
+ // 2. Add points one by one
36
+ for (let i = 0; i < points.length; i++) {
37
+ const p = points[i];
38
+ const badTriangles = [];
39
+
40
+ for (const t of triangles) {
41
+ if (isInCircumcircle(p, t)) {
42
+ badTriangles.push(t);
43
+ }
44
+ }
45
+
46
+ const polygon = [];
47
+ for (const t of badTriangles) {
48
+ const edges = [
49
+ { a: t.p1, b: t.p2, i1: t.indices[0], i2: t.indices[1] },
50
+ { a: t.p2, b: t.p3, i1: t.indices[1], i2: t.indices[2] },
51
+ { a: t.p3, b: t.p1, i1: t.indices[2], i2: t.indices[0] }
52
+ ];
53
+
54
+ for (const edge of edges) {
55
+ let isShared = false;
56
+ for (const t2 of badTriangles) {
57
+ if (t === t2) continue;
58
+ if (isSameEdge(edge, t2)) {
59
+ isShared = true;
60
+ break;
61
+ }
62
+ }
63
+ if (!isShared) {
64
+ polygon.push(edge);
65
+ }
66
+ }
67
+ }
68
+
69
+ // Remove bad triangles
70
+ triangles = triangles.filter(t => !badTriangles.includes(t));
71
+
72
+ // Add new triangles from polygon edges to point p
73
+ for (const edge of polygon) {
74
+ triangles.push({
75
+ p1: edge.a,
76
+ p2: edge.b,
77
+ p3: p,
78
+ indices: [edge.i1, edge.i2, i]
79
+ });
80
+ }
81
+ }
82
+
83
+ // 3. Remove triangles that share vertices with the super-triangle
84
+ return triangles.filter(t => {
85
+ return t.indices[0] >= 0 && t.indices[1] >= 0 && t.indices[2] >= 0;
86
+ }).map(t => t.indices);
87
+ }
88
+
89
+ function isInCircumcircle(p, t) {
90
+ const x1 = t.p1.x, y1 = t.p1.y;
91
+ const x2 = t.p2.x, y2 = t.p2.y;
92
+ const x3 = t.p3.x, y3 = t.p3.y;
93
+
94
+ const D = 2 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
95
+ const centerX = ((x1 * x1 + y1 * y1) * (y2 - y3) + (x2 * x2 + y2 * y2) * (y3 - y1) + (x3 * x3 + y3 * y3) * (y1 - y2)) / D;
96
+ const centerY = ((x1 * x1 + y1 * y1) * (x3 - x2) + (x2 * x2 + y2 * y2) * (x1 - x3) + (x3 * x3 + y3 * y3) * (x2 - x1)) / D;
97
+
98
+ const radiusSq = (x1 - centerX) * (x1 - centerX) + (y1 - centerY) * (y1 - centerY);
99
+ const distSq = (p.x - centerX) * (p.x - centerX) + (p.y - centerY) * (p.y - centerY);
100
+
101
+ return distSq <= radiusSq;
102
+ }
103
+
104
+ function isSameEdge(edge, triangle) {
105
+ const tEdges = [
106
+ [triangle.indices[0], triangle.indices[1]],
107
+ [triangle.indices[1], triangle.indices[2]],
108
+ [triangle.indices[2], triangle.indices[0]]
109
+ ];
110
+
111
+ for (const te of tEdges) {
112
+ if ((edge.i1 === te[0] && edge.i2 === te[1]) || (edge.i1 === te[1] && edge.i2 === te[0])) {
113
+ return true;
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+
119
+ /**
120
+ * Extract edges from triangles for Mass-Spring logic
121
+ */
122
+ export function getEdges(triangles) {
123
+ const edgeSet = new Set();
124
+ const edges = [];
125
+
126
+ for (const t of triangles) {
127
+ const pairs = [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]];
128
+ for (const pair of pairs) {
129
+ const low = Math.min(pair[0], pair[1]);
130
+ const high = Math.max(pair[0], pair[1]);
131
+ const key = `${low}-${high}`;
132
+ if (!edgeSet.has(key)) {
133
+ edgeSet.add(key);
134
+ edges.push([low, high]);
135
+ }
136
+ }
137
+ }
138
+ return edges;
139
+ }
@@ -262,14 +262,14 @@ class Controller {
262
262
  }
263
263
 
264
264
  async _trackAndUpdate(inputData: any, lastModelViewTransform: number[][], targetIndex: number) {
265
- const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0 } = await this._workerTrack(
265
+ const { worldCoords, screenCoords, reliabilities, indices = [], octaveIndex = 0, deformedMesh } = await this._workerTrack(
266
266
  inputData,
267
267
  lastModelViewTransform,
268
268
  targetIndex,
269
269
  );
270
270
 
271
271
  if (!worldCoords || worldCoords.length === 0) {
272
- return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [] };
272
+ return { modelViewTransform: null, screenCoords: [], reliabilities: [], stabilities: [], deformedMesh: null };
273
273
  }
274
274
 
275
275
  const state = this.trackingStates[targetIndex];
@@ -344,14 +344,16 @@ class Controller {
344
344
  stabilities: finalWorldCoords.map((_, i) => {
345
345
  const globalIdx = indices[i];
346
346
  return stabilities[globalIdx];
347
- })
347
+ }),
348
+ deformedMesh
348
349
  });
349
350
 
350
351
  return {
351
352
  modelViewTransform,
352
353
  screenCoords: finalScreenCoords,
353
354
  reliabilities: finalReliabilities,
354
- stabilities: finalStabilities
355
+ stabilities: finalStabilities,
356
+ deformedMesh
355
357
  };
356
358
  }
357
359
 
@@ -420,6 +422,7 @@ class Controller {
420
422
  trackingState.screenCoords = result.screenCoords;
421
423
  trackingState.reliabilities = result.reliabilities;
422
424
  trackingState.stabilities = result.stabilities;
425
+ (trackingState as any).deformedMesh = result.deformedMesh;
423
426
  }
424
427
  }
425
428
 
@@ -465,7 +468,8 @@ class Controller {
465
468
  modelViewTransform: trackingState.currentModelViewTransform,
466
469
  screenCoords: trackingState.screenCoords,
467
470
  reliabilities: trackingState.reliabilities,
468
- stabilities: trackingState.stabilities
471
+ stabilities: trackingState.stabilities,
472
+ deformedMesh: (trackingState as any).deformedMesh
469
473
  });
470
474
  }
471
475
  }