@srsergio/taptapp-ar 1.0.73 → 1.0.75
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/dist/compiler/controller.js +1 -1
- package/dist/compiler/detector/crop-detector.d.ts +13 -0
- package/dist/compiler/detector/crop-detector.js +40 -16
- package/dist/compiler/simple-ar.js +1 -1
- package/dist/compiler/tracker/tracker.js +44 -7
- package/dist/react/TaptappAR.js +3 -0
- package/package.json +1 -1
- package/src/compiler/controller.ts +1 -1
- package/src/compiler/detector/crop-detector.js +46 -12
- package/src/compiler/simple-ar.ts +1 -1
- package/src/compiler/tracker/tracker.js +43 -12
- package/src/react/TaptappAR.tsx +3 -0
|
@@ -18,7 +18,7 @@ const getControllerWorker = async () => {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
ControllerWorker = await getControllerWorker();
|
|
21
|
-
const DEFAULT_FILTER_CUTOFF =
|
|
21
|
+
const DEFAULT_FILTER_CUTOFF = 0.5;
|
|
22
22
|
const DEFAULT_FILTER_BETA = 0.1;
|
|
23
23
|
const DEFAULT_WARMUP_TOLERANCE = 8;
|
|
24
24
|
const DEFAULT_MISS_TOLERANCE = 2;
|
|
@@ -22,6 +22,19 @@ export class CropDetector {
|
|
|
22
22
|
projectedImage?: undefined;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Scans the ENTIRE frame by downsampling it to cropSize
|
|
27
|
+
*/
|
|
28
|
+
_detectGlobal(imageData: any): {
|
|
29
|
+
featurePoints: any[];
|
|
30
|
+
debugExtra: {
|
|
31
|
+
projectedImage: number[];
|
|
32
|
+
isGlobal: boolean;
|
|
33
|
+
} | {
|
|
34
|
+
projectedImage?: undefined;
|
|
35
|
+
isGlobal?: undefined;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
25
38
|
_detect(imageData: any, startX: any, startY: any): {
|
|
26
39
|
featurePoints: any[];
|
|
27
40
|
debugExtra: {
|
|
@@ -24,27 +24,51 @@ class CropDetector {
|
|
|
24
24
|
}
|
|
25
25
|
detectMoving(input) {
|
|
26
26
|
const imageData = input;
|
|
27
|
-
//
|
|
27
|
+
// 🚀 MOONSHOT: Alternate between local crops and GLOBAL scan
|
|
28
|
+
// This solves the "not reading the whole screen" issue.
|
|
29
|
+
// Every 3 frames, we do a full screen downsampled scan.
|
|
30
|
+
if (this.lastRandomIndex % 3 === 0) {
|
|
31
|
+
this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
|
|
32
|
+
return this._detectGlobal(imageData);
|
|
33
|
+
}
|
|
34
|
+
// Original moving crop logic for high-detail local detection
|
|
28
35
|
const gridSize = 5;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
36
|
+
const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
|
|
37
|
+
const dx = idx % gridSize;
|
|
38
|
+
const dy = Math.floor(idx / gridSize);
|
|
32
39
|
const stepX = this.cropSize / 3;
|
|
33
40
|
const stepY = this.cropSize / 3;
|
|
34
41
|
let startY = Math.floor(this.height / 2 - this.cropSize / 2 + (dy - 2) * stepY);
|
|
35
42
|
let startX = Math.floor(this.width / 2 - this.cropSize / 2 + (dx - 2) * stepX);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
43
|
+
startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
|
|
44
|
+
startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
|
|
45
|
+
this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
|
|
46
|
+
return this._detect(imageData, startX, startY);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Scans the ENTIRE frame by downsampling it to cropSize
|
|
50
|
+
*/
|
|
51
|
+
_detectGlobal(imageData) {
|
|
52
|
+
const croppedData = new Float32Array(this.cropSize * this.cropSize);
|
|
53
|
+
const scaleX = this.width / this.cropSize;
|
|
54
|
+
const scaleY = this.height / this.cropSize;
|
|
55
|
+
// Fast downsample (nearest neighbor is enough for initial feature detection)
|
|
56
|
+
for (let y = 0; y < this.cropSize; y++) {
|
|
57
|
+
const srcY = Math.floor(y * scaleY) * this.width;
|
|
58
|
+
const dstY = y * this.cropSize;
|
|
59
|
+
for (let x = 0; x < this.cropSize; x++) {
|
|
60
|
+
croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const { featurePoints } = this.detector.detect(croppedData);
|
|
64
|
+
featurePoints.forEach((p) => {
|
|
65
|
+
p.x *= scaleX;
|
|
66
|
+
p.y *= scaleY;
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
featurePoints,
|
|
70
|
+
debugExtra: this.debugMode ? { projectedImage: Array.from(croppedData), isGlobal: true } : {}
|
|
71
|
+
};
|
|
48
72
|
}
|
|
49
73
|
_detect(imageData, startX, startY) {
|
|
50
74
|
// Crop manually since imageData is now a flat array (width * height)
|
|
@@ -123,7 +123,7 @@ class SimpleAR {
|
|
|
123
123
|
}
|
|
124
124
|
this.lastMatrix = worldMatrix;
|
|
125
125
|
if (!this.filters[targetIndex]) {
|
|
126
|
-
this.filters[targetIndex] = new OneEuroFilter({ minCutOff:
|
|
126
|
+
this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.8, beta: 0.2 });
|
|
127
127
|
}
|
|
128
128
|
const flatMVT = [
|
|
129
129
|
modelViewTransform[0][0], modelViewTransform[0][1], modelViewTransform[0][2], modelViewTransform[0][3],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../estimation/utils.js";
|
|
2
2
|
const AR2_DEFAULT_TS = 6;
|
|
3
3
|
const AR2_DEFAULT_TS_GAP = 1;
|
|
4
|
-
const AR2_SEARCH_SIZE = 18
|
|
4
|
+
const AR2_SEARCH_SIZE = 34; // Increased from 18 to 34 for much better fast-motion tracking
|
|
5
5
|
const AR2_SEARCH_GAP = 1;
|
|
6
6
|
const AR2_SIM_THRESH = 0.6;
|
|
7
7
|
const TRACKING_KEYFRAME = 0; // 0: 128px (optimized)
|
|
@@ -165,18 +165,18 @@ class Tracker {
|
|
|
165
165
|
matchingPoints.push([bestX, bestY]);
|
|
166
166
|
continue;
|
|
167
167
|
}
|
|
168
|
-
// Search
|
|
169
|
-
|
|
168
|
+
// 🚀 MOONSHOT: Coarse-to-Fine Search for MAXIMUM FPS
|
|
169
|
+
// Step 1: Coarse search (Gap 4)
|
|
170
|
+
const coarseGap = 4;
|
|
171
|
+
for (let sy = -searchOneSize; sy <= searchOneSize; sy += coarseGap) {
|
|
170
172
|
const cy = sCenterY + sy;
|
|
171
173
|
if (cy < templateOneSize || cy >= markerHeight - templateOneSize)
|
|
172
174
|
continue;
|
|
173
|
-
for (let sx = -searchOneSize; sx <= searchOneSize; sx +=
|
|
175
|
+
for (let sx = -searchOneSize; sx <= searchOneSize; sx += coarseGap) {
|
|
174
176
|
const cx = sCenterX + sx;
|
|
175
177
|
if (cx < templateOneSize || cx >= markerWidth - templateOneSize)
|
|
176
178
|
continue;
|
|
177
|
-
let sumI = 0;
|
|
178
|
-
let sumI2 = 0;
|
|
179
|
-
let sumIT = 0;
|
|
179
|
+
let sumI = 0, sumI2 = 0, sumIT = 0;
|
|
180
180
|
for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
|
|
181
181
|
const rowOffset = (cy + ty) * markerWidth;
|
|
182
182
|
const tRowOffset = (ty + templateOneSize) * templateSize;
|
|
@@ -199,6 +199,43 @@ class Tracker {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
// Step 2: Fine refinement (Gap 1) only around the best coarse match
|
|
203
|
+
if (bestSim > AR2_SIM_THRESH) {
|
|
204
|
+
const fineCenterX = (bestX * scale) | 0;
|
|
205
|
+
const fineCenterY = (bestY * scale) | 0;
|
|
206
|
+
const fineSearch = coarseGap;
|
|
207
|
+
for (let sy = -fineSearch; sy <= fineSearch; sy++) {
|
|
208
|
+
const cy = fineCenterY + sy;
|
|
209
|
+
if (cy < templateOneSize || cy >= markerHeight - templateOneSize)
|
|
210
|
+
continue;
|
|
211
|
+
for (let sx = -fineSearch; sx <= fineSearch; sx++) {
|
|
212
|
+
const cx = fineCenterX + sx;
|
|
213
|
+
if (cx < templateOneSize || cx >= markerWidth - templateOneSize)
|
|
214
|
+
continue;
|
|
215
|
+
let sumI = 0, sumI2 = 0, sumIT = 0;
|
|
216
|
+
for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
|
|
217
|
+
const rowOffset = (cy + ty) * markerWidth;
|
|
218
|
+
const tRowOffset = (ty + templateOneSize) * templateSize;
|
|
219
|
+
for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
|
|
220
|
+
const valI = projectedImage[rowOffset + (cx + tx)];
|
|
221
|
+
const valT = templateData[tRowOffset + (tx + templateOneSize)];
|
|
222
|
+
sumI += valI;
|
|
223
|
+
sumI2 += valI * valI;
|
|
224
|
+
sumIT += valI * valT;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const varI = Math.sqrt(Math.max(0, sumI2 - (sumI * sumI) * oneOverNPixels));
|
|
228
|
+
if (varI < 0.0001)
|
|
229
|
+
continue;
|
|
230
|
+
const sim = (sumIT - (sumI * sumT) * oneOverNPixels) / (varI * varT);
|
|
231
|
+
if (sim > bestSim) {
|
|
232
|
+
bestSim = sim;
|
|
233
|
+
bestX = cx / scale;
|
|
234
|
+
bestY = cy / scale;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
202
239
|
sims[f] = bestSim;
|
|
203
240
|
matchingPoints.push([bestX, bestY]);
|
|
204
241
|
}
|
package/dist/react/TaptappAR.js
CHANGED
|
@@ -92,6 +92,9 @@ export const TaptappAR = ({ config, className = "", showScanningOverlay = true,
|
|
|
92
92
|
display: block;
|
|
93
93
|
width: 100%;
|
|
94
94
|
height: auto;
|
|
95
|
+
will-change: transform;
|
|
96
|
+
/* Visual interpolation to smooth out frame gaps */
|
|
97
|
+
transition: transform 0.1s linear, opacity 0.3s ease;
|
|
95
98
|
}
|
|
96
99
|
` })] }));
|
|
97
100
|
};
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ const getControllerWorker = async () => {
|
|
|
19
19
|
};
|
|
20
20
|
ControllerWorker = await getControllerWorker();
|
|
21
21
|
|
|
22
|
-
const DEFAULT_FILTER_CUTOFF =
|
|
22
|
+
const DEFAULT_FILTER_CUTOFF = 0.5;
|
|
23
23
|
const DEFAULT_FILTER_BETA = 0.1;
|
|
24
24
|
const DEFAULT_WARMUP_TOLERANCE = 8;
|
|
25
25
|
const DEFAULT_MISS_TOLERANCE = 2;
|
|
@@ -33,28 +33,62 @@ class CropDetector {
|
|
|
33
33
|
detectMoving(input) {
|
|
34
34
|
const imageData = input;
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// 🚀 MOONSHOT: Alternate between local crops and GLOBAL scan
|
|
37
|
+
// This solves the "not reading the whole screen" issue.
|
|
38
|
+
// Every 3 frames, we do a full screen downsampled scan.
|
|
39
|
+
if (this.lastRandomIndex % 3 === 0) {
|
|
40
|
+
this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
|
|
41
|
+
return this._detectGlobal(imageData);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Original moving crop logic for high-detail local detection
|
|
37
45
|
const gridSize = 5;
|
|
38
|
-
const
|
|
39
|
-
const
|
|
46
|
+
const idx = (this.lastRandomIndex - 1) % (gridSize * gridSize);
|
|
47
|
+
const dx = idx % gridSize;
|
|
48
|
+
const dy = Math.floor(idx / gridSize);
|
|
40
49
|
|
|
41
|
-
// Calculate offset from center, with overlap for better detection
|
|
42
50
|
const stepX = this.cropSize / 3;
|
|
43
51
|
const stepY = this.cropSize / 3;
|
|
44
52
|
|
|
45
53
|
let startY = Math.floor(this.height / 2 - this.cropSize / 2 + (dy - 2) * stepY);
|
|
46
54
|
let startX = Math.floor(this.width / 2 - this.cropSize / 2 + (dx - 2) * stepX);
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (startY < 0) startY = 0;
|
|
51
|
-
if (startX >= this.width - this.cropSize) startX = this.width - this.cropSize - 1;
|
|
52
|
-
if (startY >= this.height - this.cropSize) startY = this.height - this.cropSize - 1;
|
|
56
|
+
startX = Math.max(0, Math.min(this.width - this.cropSize - 1, startX));
|
|
57
|
+
startY = Math.max(0, Math.min(this.height - this.cropSize - 1, startY));
|
|
53
58
|
|
|
54
|
-
this.lastRandomIndex = (this.lastRandomIndex + 1) %
|
|
59
|
+
this.lastRandomIndex = (this.lastRandomIndex + 1) % 25;
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
return this._detect(imageData, startX, startY);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Scans the ENTIRE frame by downsampling it to cropSize
|
|
66
|
+
*/
|
|
67
|
+
_detectGlobal(imageData) {
|
|
68
|
+
const croppedData = new Float32Array(this.cropSize * this.cropSize);
|
|
69
|
+
const scaleX = this.width / this.cropSize;
|
|
70
|
+
const scaleY = this.height / this.cropSize;
|
|
71
|
+
|
|
72
|
+
// Fast downsample (nearest neighbor is enough for initial feature detection)
|
|
73
|
+
for (let y = 0; y < this.cropSize; y++) {
|
|
74
|
+
const srcY = Math.floor(y * scaleY) * this.width;
|
|
75
|
+
const dstY = y * this.cropSize;
|
|
76
|
+
for (let x = 0; x < this.cropSize; x++) {
|
|
77
|
+
croppedData[dstY + x] = imageData[srcY + Math.floor(x * scaleX)];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { featurePoints } = this.detector.detect(croppedData);
|
|
82
|
+
|
|
83
|
+
featurePoints.forEach((p) => {
|
|
84
|
+
p.x *= scaleX;
|
|
85
|
+
p.y *= scaleY;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
featurePoints,
|
|
90
|
+
debugExtra: this.debugMode ? { projectedImage: Array.from(croppedData), isGlobal: true } : {}
|
|
91
|
+
};
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
_detect(imageData, startX, startY) {
|
|
@@ -163,7 +163,7 @@ class SimpleAR {
|
|
|
163
163
|
this.lastMatrix = worldMatrix;
|
|
164
164
|
|
|
165
165
|
if (!this.filters[targetIndex]) {
|
|
166
|
-
this.filters[targetIndex] = new OneEuroFilter({ minCutOff:
|
|
166
|
+
this.filters[targetIndex] = new OneEuroFilter({ minCutOff: 0.8, beta: 0.2 });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
const flatMVT = [
|
|
@@ -2,7 +2,7 @@ import { buildModelViewProjectionTransform, computeScreenCoordiate } from "../es
|
|
|
2
2
|
|
|
3
3
|
const AR2_DEFAULT_TS = 6;
|
|
4
4
|
const AR2_DEFAULT_TS_GAP = 1;
|
|
5
|
-
const AR2_SEARCH_SIZE = 18
|
|
5
|
+
const AR2_SEARCH_SIZE = 34; // Increased from 18 to 34 for much better fast-motion tracking
|
|
6
6
|
const AR2_SEARCH_GAP = 1;
|
|
7
7
|
const AR2_SIM_THRESH = 0.6;
|
|
8
8
|
|
|
@@ -214,29 +214,25 @@ class Tracker {
|
|
|
214
214
|
continue;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
// Search
|
|
218
|
-
|
|
217
|
+
// 🚀 MOONSHOT: Coarse-to-Fine Search for MAXIMUM FPS
|
|
218
|
+
// Step 1: Coarse search (Gap 4)
|
|
219
|
+
const coarseGap = 4;
|
|
220
|
+
for (let sy = -searchOneSize; sy <= searchOneSize; sy += coarseGap) {
|
|
219
221
|
const cy = sCenterY + sy;
|
|
220
222
|
if (cy < templateOneSize || cy >= markerHeight - templateOneSize) continue;
|
|
221
223
|
|
|
222
|
-
for (let sx = -searchOneSize; sx <= searchOneSize; sx +=
|
|
224
|
+
for (let sx = -searchOneSize; sx <= searchOneSize; sx += coarseGap) {
|
|
223
225
|
const cx = sCenterX + sx;
|
|
224
226
|
if (cx < templateOneSize || cx >= markerWidth - templateOneSize) continue;
|
|
225
227
|
|
|
226
|
-
let sumI = 0;
|
|
227
|
-
let sumI2 = 0;
|
|
228
|
-
let sumIT = 0;
|
|
229
|
-
|
|
228
|
+
let sumI = 0, sumI2 = 0, sumIT = 0;
|
|
230
229
|
for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
|
|
231
230
|
const rowOffset = (cy + ty) * markerWidth;
|
|
232
231
|
const tRowOffset = (ty + templateOneSize) * templateSize;
|
|
233
232
|
for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
|
|
234
233
|
const valI = projectedImage[rowOffset + (cx + tx)];
|
|
235
234
|
const valT = templateData[tRowOffset + (tx + templateOneSize)];
|
|
236
|
-
|
|
237
|
-
sumI += valI;
|
|
238
|
-
sumI2 += valI * valI;
|
|
239
|
-
sumIT += valI * valT;
|
|
235
|
+
sumI += valI; sumI2 += valI * valI; sumIT += valI * valT;
|
|
240
236
|
}
|
|
241
237
|
}
|
|
242
238
|
|
|
@@ -252,6 +248,41 @@ class Tracker {
|
|
|
252
248
|
}
|
|
253
249
|
}
|
|
254
250
|
|
|
251
|
+
// Step 2: Fine refinement (Gap 1) only around the best coarse match
|
|
252
|
+
if (bestSim > AR2_SIM_THRESH) {
|
|
253
|
+
const fineCenterX = (bestX * scale) | 0;
|
|
254
|
+
const fineCenterY = (bestY * scale) | 0;
|
|
255
|
+
const fineSearch = coarseGap;
|
|
256
|
+
|
|
257
|
+
for (let sy = -fineSearch; sy <= fineSearch; sy++) {
|
|
258
|
+
const cy = fineCenterY + sy;
|
|
259
|
+
if (cy < templateOneSize || cy >= markerHeight - templateOneSize) continue;
|
|
260
|
+
for (let sx = -fineSearch; sx <= fineSearch; sx++) {
|
|
261
|
+
const cx = fineCenterX + sx;
|
|
262
|
+
if (cx < templateOneSize || cx >= markerWidth - templateOneSize) continue;
|
|
263
|
+
|
|
264
|
+
let sumI = 0, sumI2 = 0, sumIT = 0;
|
|
265
|
+
for (let ty = -templateOneSize; ty <= templateOneSize; ty++) {
|
|
266
|
+
const rowOffset = (cy + ty) * markerWidth;
|
|
267
|
+
const tRowOffset = (ty + templateOneSize) * templateSize;
|
|
268
|
+
for (let tx = -templateOneSize; tx <= templateOneSize; tx++) {
|
|
269
|
+
const valI = projectedImage[rowOffset + (cx + tx)];
|
|
270
|
+
const valT = templateData[tRowOffset + (tx + templateOneSize)];
|
|
271
|
+
sumI += valI; sumI2 += valI * valI; sumIT += valI * valT;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const varI = Math.sqrt(Math.max(0, sumI2 - (sumI * sumI) * oneOverNPixels));
|
|
275
|
+
if (varI < 0.0001) continue;
|
|
276
|
+
const sim = (sumIT - (sumI * sumT) * oneOverNPixels) / (varI * varT);
|
|
277
|
+
if (sim > bestSim) {
|
|
278
|
+
bestSim = sim;
|
|
279
|
+
bestX = cx / scale;
|
|
280
|
+
bestY = cy / scale;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
255
286
|
sims[f] = bestSim;
|
|
256
287
|
matchingPoints.push([bestX, bestY]);
|
|
257
288
|
}
|
package/src/react/TaptappAR.tsx
CHANGED
|
@@ -169,6 +169,9 @@ export const TaptappAR: React.FC<TaptappARProps> = ({
|
|
|
169
169
|
display: block;
|
|
170
170
|
width: 100%;
|
|
171
171
|
height: auto;
|
|
172
|
+
will-change: transform;
|
|
173
|
+
/* Visual interpolation to smooth out frame gaps */
|
|
174
|
+
transition: transform 0.1s linear, opacity 0.3s ease;
|
|
172
175
|
}
|
|
173
176
|
`}</style>
|
|
174
177
|
</div>
|