@srsergio/taptapp-ar 1.0.92 → 1.0.94
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 +16 -14
- package/dist/compiler/offline-compiler.d.ts +3 -3
- package/dist/compiler/offline-compiler.js +50 -33
- package/dist/core/constants.d.ts +2 -0
- package/dist/core/constants.js +4 -1
- package/dist/core/detector/detector-lite.d.ts +6 -5
- package/dist/core/detector/detector-lite.js +46 -16
- package/dist/core/image-list.d.ts +24 -6
- package/dist/core/image-list.js +4 -4
- package/dist/core/matching/matcher.d.ts +1 -1
- package/dist/core/matching/matcher.js +7 -4
- package/dist/core/matching/matching.d.ts +2 -1
- package/dist/core/matching/matching.js +43 -11
- package/dist/core/perception/bio-inspired-engine.d.ts +130 -0
- package/dist/core/perception/bio-inspired-engine.js +232 -0
- package/dist/core/perception/foveal-attention.d.ts +142 -0
- package/dist/core/perception/foveal-attention.js +280 -0
- package/dist/core/perception/index.d.ts +6 -0
- package/dist/core/perception/index.js +17 -0
- package/dist/core/perception/predictive-coding.d.ts +92 -0
- package/dist/core/perception/predictive-coding.js +278 -0
- package/dist/core/perception/saccadic-controller.d.ts +126 -0
- package/dist/core/perception/saccadic-controller.js +269 -0
- package/dist/core/perception/saliency-map.d.ts +74 -0
- package/dist/core/perception/saliency-map.js +254 -0
- package/dist/core/perception/scale-orchestrator.d.ts +28 -0
- package/dist/core/perception/scale-orchestrator.js +68 -0
- package/dist/core/protocol.d.ts +14 -1
- package/dist/core/protocol.js +33 -1
- package/dist/runtime/bio-inspired-controller.d.ts +135 -0
- package/dist/runtime/bio-inspired-controller.js +358 -0
- package/dist/runtime/controller.d.ts +11 -2
- package/dist/runtime/controller.js +20 -8
- package/dist/runtime/controller.worker.js +2 -2
- package/package.json +1 -1
- package/src/compiler/offline-compiler.ts +56 -36
- package/src/core/constants.ts +5 -1
- package/src/core/detector/detector-lite.js +46 -16
- package/src/core/image-list.js +4 -4
- package/src/core/matching/matcher.js +8 -4
- package/src/core/matching/matching.js +51 -12
- package/src/core/perception/bio-inspired-engine.js +275 -0
- package/src/core/perception/foveal-attention.js +306 -0
- package/src/core/perception/index.js +18 -0
- package/src/core/perception/predictive-coding.js +327 -0
- package/src/core/perception/saccadic-controller.js +303 -0
- package/src/core/perception/saliency-map.js +296 -0
- package/src/core/perception/scale-orchestrator.js +80 -0
- package/src/core/protocol.ts +38 -1
- package/src/runtime/bio-inspired-controller.ts +448 -0
- package/src/runtime/controller.ts +22 -7
- package/src/runtime/controller.worker.js +2 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saccadic Controller
|
|
3
|
+
*
|
|
4
|
+
* Mimics human eye saccades - rapid movements that redirect foveal attention
|
|
5
|
+
* to areas of interest. The human eye makes 3-4 saccades per second to
|
|
6
|
+
* build a complete picture of the visual scene.
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Compute saliency map to find "interesting" regions
|
|
10
|
+
* 2. Use tracking state to predict where features should be
|
|
11
|
+
* 3. Generate priority-ordered list of "glance" targets
|
|
12
|
+
* 4. Limit saccades per frame to balance coverage vs. efficiency
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A saccade target representing where attention should be directed
|
|
17
|
+
* @typedef {Object} SaccadeTarget
|
|
18
|
+
* @property {number} x - X coordinate
|
|
19
|
+
* @property {number} y - Y coordinate
|
|
20
|
+
* @property {number} priority - Priority (0 = highest)
|
|
21
|
+
* @property {string} reason - Why this target was selected
|
|
22
|
+
* @property {number} saliency - Saliency score at this location
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
class SaccadicController {
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} width - Image width
|
|
28
|
+
* @param {number} height - Image height
|
|
29
|
+
* @param {Object} config - Configuration
|
|
30
|
+
*/
|
|
31
|
+
constructor(width, height, config) {
|
|
32
|
+
this.width = width;
|
|
33
|
+
this.height = height;
|
|
34
|
+
this.config = config;
|
|
35
|
+
|
|
36
|
+
// Saccade history for inhibition of return
|
|
37
|
+
this.recentTargets = [];
|
|
38
|
+
this.inhibitionRadius = Math.min(width, height) * 0.1;
|
|
39
|
+
|
|
40
|
+
// Movement prediction
|
|
41
|
+
this.velocityHistory = [];
|
|
42
|
+
this.lastCenter = { x: width / 2, y: height / 2 };
|
|
43
|
+
|
|
44
|
+
// Grid for systematic coverage
|
|
45
|
+
this.gridCells = this._buildCoverageGrid(3, 3);
|
|
46
|
+
this.lastVisitedCell = 4; // Center
|
|
47
|
+
|
|
48
|
+
// State
|
|
49
|
+
this.lastSaccadeTime = 0;
|
|
50
|
+
this.saccadeCount = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a grid for systematic coverage during tracking loss
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
_buildCoverageGrid(rows, cols) {
|
|
58
|
+
const cells = [];
|
|
59
|
+
const cellW = this.width / cols;
|
|
60
|
+
const cellH = this.height / rows;
|
|
61
|
+
|
|
62
|
+
for (let r = 0; r < rows; r++) {
|
|
63
|
+
for (let c = 0; c < cols; c++) {
|
|
64
|
+
cells.push({
|
|
65
|
+
x: cellW * (c + 0.5),
|
|
66
|
+
y: cellH * (r + 0.5),
|
|
67
|
+
index: r * cols + c,
|
|
68
|
+
lastVisit: 0,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return cells;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compute saccade targets based on current state
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} saliency - Saliency map result
|
|
79
|
+
* @param {Object} currentFovea - Current fovea center {x, y}
|
|
80
|
+
* @param {Object} trackingState - Current tracking state (optional)
|
|
81
|
+
* @returns {SaccadeTarget[]} Priority-ordered list of targets
|
|
82
|
+
*/
|
|
83
|
+
computeTargets(saliency, currentFovea, trackingState = null) {
|
|
84
|
+
const targets = [];
|
|
85
|
+
const maxTargets = this.config.MAX_SACCADES_PER_FRAME;
|
|
86
|
+
|
|
87
|
+
// Strategy 1: Follow tracking prediction (highest priority)
|
|
88
|
+
if (trackingState && trackingState.isTracking) {
|
|
89
|
+
const predicted = this._predictTrackingCenter(trackingState);
|
|
90
|
+
if (predicted) {
|
|
91
|
+
targets.push({
|
|
92
|
+
x: predicted.x,
|
|
93
|
+
y: predicted.y,
|
|
94
|
+
priority: 0,
|
|
95
|
+
reason: 'tracking_prediction',
|
|
96
|
+
saliency: 1.0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Strategy 2: High saliency regions
|
|
102
|
+
if (saliency && saliency.peaks) {
|
|
103
|
+
for (const peak of saliency.peaks) {
|
|
104
|
+
if (targets.length >= maxTargets) break;
|
|
105
|
+
|
|
106
|
+
// Skip if too close to existing targets (inhibition of return)
|
|
107
|
+
if (this._isInhibited(peak.x, peak.y, targets)) continue;
|
|
108
|
+
|
|
109
|
+
if (peak.value > this.config.SALIENCY_THRESHOLD) {
|
|
110
|
+
targets.push({
|
|
111
|
+
x: peak.x,
|
|
112
|
+
y: peak.y,
|
|
113
|
+
priority: targets.length,
|
|
114
|
+
reason: 'saliency_peak',
|
|
115
|
+
saliency: peak.value,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Strategy 3: Systematic grid search (when not tracking)
|
|
122
|
+
if (!trackingState?.isTracking && targets.length < maxTargets) {
|
|
123
|
+
const gridTarget = this._getNextGridCell();
|
|
124
|
+
if (gridTarget && !this._isInhibited(gridTarget.x, gridTarget.y, targets)) {
|
|
125
|
+
targets.push({
|
|
126
|
+
x: gridTarget.x,
|
|
127
|
+
y: gridTarget.y,
|
|
128
|
+
priority: targets.length,
|
|
129
|
+
reason: 'grid_search',
|
|
130
|
+
saliency: 0.5,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Strategy 4: Stay at current center if no better options
|
|
136
|
+
if (targets.length === 0) {
|
|
137
|
+
targets.push({
|
|
138
|
+
x: currentFovea.x,
|
|
139
|
+
y: currentFovea.y,
|
|
140
|
+
priority: 0,
|
|
141
|
+
reason: 'maintain_position',
|
|
142
|
+
saliency: 0.3,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Update history
|
|
147
|
+
this._updateHistory(targets);
|
|
148
|
+
|
|
149
|
+
return targets;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Predict center of tracking based on current state and velocity
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_predictTrackingCenter(trackingState) {
|
|
157
|
+
if (!trackingState.worldMatrix) return null;
|
|
158
|
+
|
|
159
|
+
// Extract center from world matrix
|
|
160
|
+
const matrix = trackingState.worldMatrix;
|
|
161
|
+
const cx = matrix[12] || this.width / 2;
|
|
162
|
+
const cy = matrix[13] || this.height / 2;
|
|
163
|
+
|
|
164
|
+
// Apply velocity-based prediction
|
|
165
|
+
if (this.velocityHistory.length >= 2) {
|
|
166
|
+
const vx = this._computeAverageVelocity('x');
|
|
167
|
+
const vy = this._computeAverageVelocity('y');
|
|
168
|
+
|
|
169
|
+
// Predict 1 frame ahead
|
|
170
|
+
return {
|
|
171
|
+
x: Math.max(0, Math.min(this.width - 1, cx + vx)),
|
|
172
|
+
y: Math.max(0, Math.min(this.height - 1, cy + vy)),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { x: cx, y: cy };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Compute average velocity from history
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_computeAverageVelocity(axis) {
|
|
184
|
+
if (this.velocityHistory.length < 2) return 0;
|
|
185
|
+
|
|
186
|
+
let sum = 0;
|
|
187
|
+
for (let i = 1; i < this.velocityHistory.length; i++) {
|
|
188
|
+
sum += this.velocityHistory[i][axis] - this.velocityHistory[i - 1][axis];
|
|
189
|
+
}
|
|
190
|
+
return sum / (this.velocityHistory.length - 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if a location is inhibited (too close to recent targets)
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_isInhibited(x, y, currentTargets) {
|
|
198
|
+
const r2 = this.inhibitionRadius ** 2;
|
|
199
|
+
|
|
200
|
+
// Check against current frame targets
|
|
201
|
+
for (const t of currentTargets) {
|
|
202
|
+
const dx = x - t.x;
|
|
203
|
+
const dy = y - t.y;
|
|
204
|
+
if (dx * dx + dy * dy < r2) return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check against recent history
|
|
208
|
+
for (const t of this.recentTargets) {
|
|
209
|
+
const dx = x - t.x;
|
|
210
|
+
const dy = y - t.y;
|
|
211
|
+
if (dx * dx + dy * dy < r2) return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get next grid cell for systematic search
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
_getNextGridCell() {
|
|
222
|
+
// Find least recently visited cell
|
|
223
|
+
let oldest = this.gridCells[0];
|
|
224
|
+
let oldestTime = Infinity;
|
|
225
|
+
|
|
226
|
+
for (const cell of this.gridCells) {
|
|
227
|
+
if (cell.lastVisit < oldestTime) {
|
|
228
|
+
oldestTime = cell.lastVisit;
|
|
229
|
+
oldest = cell;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
oldest.lastVisit = Date.now();
|
|
234
|
+
return oldest;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Update history with new targets
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
_updateHistory(targets) {
|
|
242
|
+
// Add to recent targets for inhibition of return
|
|
243
|
+
this.recentTargets.push(...targets);
|
|
244
|
+
|
|
245
|
+
// Keep only last N targets
|
|
246
|
+
const maxHistory = this.config.MOTION_HISTORY_FRAMES * this.config.MAX_SACCADES_PER_FRAME;
|
|
247
|
+
while (this.recentTargets.length > maxHistory) {
|
|
248
|
+
this.recentTargets.shift();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Update velocity history
|
|
252
|
+
if (targets.length > 0) {
|
|
253
|
+
this.velocityHistory.push({ x: targets[0].x, y: targets[0].y });
|
|
254
|
+
while (this.velocityHistory.length > this.config.MOTION_HISTORY_FRAMES) {
|
|
255
|
+
this.velocityHistory.shift();
|
|
256
|
+
}
|
|
257
|
+
this.lastCenter = { x: targets[0].x, y: targets[0].y };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.saccadeCount += targets.length;
|
|
261
|
+
this.lastSaccadeTime = Date.now();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the most likely location of interest based on history
|
|
266
|
+
* @returns {Object} {x, y} of predicted location
|
|
267
|
+
*/
|
|
268
|
+
getPredictedLocation() {
|
|
269
|
+
if (this.velocityHistory.length >= 2) {
|
|
270
|
+
const vx = this._computeAverageVelocity('x');
|
|
271
|
+
const vy = this._computeAverageVelocity('y');
|
|
272
|
+
return {
|
|
273
|
+
x: Math.max(0, Math.min(this.width - 1, this.lastCenter.x + vx)),
|
|
274
|
+
y: Math.max(0, Math.min(this.height - 1, this.lastCenter.y + vy)),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return this.lastCenter;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Reset controller state
|
|
282
|
+
*/
|
|
283
|
+
reset() {
|
|
284
|
+
this.recentTargets = [];
|
|
285
|
+
this.velocityHistory = [];
|
|
286
|
+
this.lastCenter = { x: this.width / 2, y: this.height / 2 };
|
|
287
|
+
this.saccadeCount = 0;
|
|
288
|
+
|
|
289
|
+
for (const cell of this.gridCells) {
|
|
290
|
+
cell.lastVisit = 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Update configuration
|
|
296
|
+
*/
|
|
297
|
+
configure(config) {
|
|
298
|
+
this.config = { ...this.config, ...config };
|
|
299
|
+
this.inhibitionRadius = Math.min(this.width, this.height) * 0.1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { SaccadicController };
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saliency Map Computation
|
|
3
|
+
*
|
|
4
|
+
* Computes visual saliency - regions that "pop out" and attract attention.
|
|
5
|
+
* Used to guide saccadic attention to visually important areas.
|
|
6
|
+
*
|
|
7
|
+
* Implements a simplified Itti-Koch saliency model:
|
|
8
|
+
* - Intensity contrast
|
|
9
|
+
* - Edge density
|
|
10
|
+
* - Local complexity
|
|
11
|
+
*
|
|
12
|
+
* For AR tracking, high-saliency regions often contain:
|
|
13
|
+
* - Corners and edges (good for feature detection)
|
|
14
|
+
* - High-contrast areas (robust to lighting changes)
|
|
15
|
+
* - Texture-rich regions (distinctive for matching)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class SaliencyMap {
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} width - Image width
|
|
21
|
+
* @param {number} height - Image height
|
|
22
|
+
*/
|
|
23
|
+
constructor(width, height) {
|
|
24
|
+
this.width = width;
|
|
25
|
+
this.height = height;
|
|
26
|
+
|
|
27
|
+
// Downsampled dimensions for efficiency
|
|
28
|
+
this.scale = 8; // Process at 1/8 resolution
|
|
29
|
+
this.scaledW = Math.ceil(width / this.scale);
|
|
30
|
+
this.scaledH = Math.ceil(height / this.scale);
|
|
31
|
+
|
|
32
|
+
// Pre-allocate buffers
|
|
33
|
+
this.intensityMap = new Float32Array(this.scaledW * this.scaledH);
|
|
34
|
+
this.contrastMap = new Float32Array(this.scaledW * this.scaledH);
|
|
35
|
+
this.edgeMap = new Float32Array(this.scaledW * this.scaledH);
|
|
36
|
+
this.saliencyBuffer = new Float32Array(this.scaledW * this.scaledH);
|
|
37
|
+
|
|
38
|
+
// Peak detection parameters
|
|
39
|
+
this.maxPeaks = 5;
|
|
40
|
+
this.suppressionRadius = Math.max(this.scaledW, this.scaledH) * 0.15;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compute saliency map for input image
|
|
45
|
+
*
|
|
46
|
+
* @param {Uint8Array} inputData - Grayscale input image
|
|
47
|
+
* @returns {Object} Saliency result with peaks
|
|
48
|
+
*/
|
|
49
|
+
compute(inputData) {
|
|
50
|
+
// Step 1: Downsample and compute intensity
|
|
51
|
+
this._downsample(inputData);
|
|
52
|
+
|
|
53
|
+
// Step 2: Compute features
|
|
54
|
+
this._computeContrast();
|
|
55
|
+
this._computeEdges();
|
|
56
|
+
|
|
57
|
+
// Step 3: Combine into saliency map
|
|
58
|
+
this._combineSaliency();
|
|
59
|
+
|
|
60
|
+
// Step 4: Find peaks
|
|
61
|
+
const peaks = this._findPeaks();
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
map: this.saliencyBuffer,
|
|
65
|
+
width: this.scaledW,
|
|
66
|
+
height: this.scaledH,
|
|
67
|
+
peaks,
|
|
68
|
+
maxSaliency: peaks.length > 0 ? peaks[0].value : 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Downsample input to working resolution
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
_downsample(inputData) {
|
|
77
|
+
const s = this.scale;
|
|
78
|
+
const w = this.width;
|
|
79
|
+
|
|
80
|
+
for (let sy = 0; sy < this.scaledH; sy++) {
|
|
81
|
+
const yStart = sy * s;
|
|
82
|
+
const yEnd = Math.min(yStart + s, this.height);
|
|
83
|
+
|
|
84
|
+
for (let sx = 0; sx < this.scaledW; sx++) {
|
|
85
|
+
const xStart = sx * s;
|
|
86
|
+
const xEnd = Math.min(xStart + s, this.width);
|
|
87
|
+
|
|
88
|
+
let sum = 0;
|
|
89
|
+
let count = 0;
|
|
90
|
+
|
|
91
|
+
for (let y = yStart; y < yEnd; y++) {
|
|
92
|
+
const rowOffset = y * w;
|
|
93
|
+
for (let x = xStart; x < xEnd; x++) {
|
|
94
|
+
sum += inputData[rowOffset + x];
|
|
95
|
+
count++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.intensityMap[sy * this.scaledW + sx] = sum / count / 255;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compute local contrast map
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_computeContrast() {
|
|
109
|
+
const w = this.scaledW;
|
|
110
|
+
const h = this.scaledH;
|
|
111
|
+
const intensity = this.intensityMap;
|
|
112
|
+
const contrast = this.contrastMap;
|
|
113
|
+
|
|
114
|
+
// 3x3 local contrast using center-surround
|
|
115
|
+
for (let y = 1; y < h - 1; y++) {
|
|
116
|
+
for (let x = 1; x < w - 1; x++) {
|
|
117
|
+
const idx = y * w + x;
|
|
118
|
+
const center = intensity[idx];
|
|
119
|
+
|
|
120
|
+
// Compute average of 8 neighbors
|
|
121
|
+
let surround = 0;
|
|
122
|
+
surround += intensity[(y - 1) * w + (x - 1)];
|
|
123
|
+
surround += intensity[(y - 1) * w + x];
|
|
124
|
+
surround += intensity[(y - 1) * w + (x + 1)];
|
|
125
|
+
surround += intensity[y * w + (x - 1)];
|
|
126
|
+
surround += intensity[y * w + (x + 1)];
|
|
127
|
+
surround += intensity[(y + 1) * w + (x - 1)];
|
|
128
|
+
surround += intensity[(y + 1) * w + x];
|
|
129
|
+
surround += intensity[(y + 1) * w + (x + 1)];
|
|
130
|
+
surround /= 8;
|
|
131
|
+
|
|
132
|
+
// Contrast is absolute difference
|
|
133
|
+
contrast[idx] = Math.abs(center - surround);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle borders
|
|
138
|
+
for (let y = 0; y < h; y++) {
|
|
139
|
+
contrast[y * w] = 0;
|
|
140
|
+
contrast[y * w + w - 1] = 0;
|
|
141
|
+
}
|
|
142
|
+
for (let x = 0; x < w; x++) {
|
|
143
|
+
contrast[x] = 0;
|
|
144
|
+
contrast[(h - 1) * w + x] = 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Compute edge density map using Sobel-like operator
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
_computeEdges() {
|
|
153
|
+
const w = this.scaledW;
|
|
154
|
+
const h = this.scaledH;
|
|
155
|
+
const intensity = this.intensityMap;
|
|
156
|
+
const edges = this.edgeMap;
|
|
157
|
+
|
|
158
|
+
for (let y = 1; y < h - 1; y++) {
|
|
159
|
+
for (let x = 1; x < w - 1; x++) {
|
|
160
|
+
// Simplified Sobel
|
|
161
|
+
const gx =
|
|
162
|
+
-intensity[(y - 1) * w + (x - 1)] + intensity[(y - 1) * w + (x + 1)] +
|
|
163
|
+
-2 * intensity[y * w + (x - 1)] + 2 * intensity[y * w + (x + 1)] +
|
|
164
|
+
-intensity[(y + 1) * w + (x - 1)] + intensity[(y + 1) * w + (x + 1)];
|
|
165
|
+
|
|
166
|
+
const gy =
|
|
167
|
+
-intensity[(y - 1) * w + (x - 1)] - 2 * intensity[(y - 1) * w + x] - intensity[(y - 1) * w + (x + 1)] +
|
|
168
|
+
intensity[(y + 1) * w + (x - 1)] + 2 * intensity[(y + 1) * w + x] + intensity[(y + 1) * w + (x + 1)];
|
|
169
|
+
|
|
170
|
+
edges[y * w + x] = Math.sqrt(gx * gx + gy * gy) / 4; // Normalize
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle borders
|
|
175
|
+
for (let y = 0; y < h; y++) {
|
|
176
|
+
edges[y * w] = 0;
|
|
177
|
+
edges[y * w + w - 1] = 0;
|
|
178
|
+
}
|
|
179
|
+
for (let x = 0; x < w; x++) {
|
|
180
|
+
edges[x] = 0;
|
|
181
|
+
edges[(h - 1) * w + x] = 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Combine features into final saliency map
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_combineSaliency() {
|
|
190
|
+
const n = this.saliencyBuffer.length;
|
|
191
|
+
const contrast = this.contrastMap;
|
|
192
|
+
const edges = this.edgeMap;
|
|
193
|
+
const saliency = this.saliencyBuffer;
|
|
194
|
+
|
|
195
|
+
// Weight: 60% contrast, 40% edges
|
|
196
|
+
for (let i = 0; i < n; i++) {
|
|
197
|
+
saliency[i] = contrast[i] * 0.6 + edges[i] * 0.4;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Normalize to [0, 1]
|
|
201
|
+
let max = 0;
|
|
202
|
+
for (let i = 0; i < n; i++) {
|
|
203
|
+
max = Math.max(max, saliency[i]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (max > 0) {
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
saliency[i] /= max;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Find peaks in saliency map using non-maximum suppression
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_findPeaks() {
|
|
218
|
+
const w = this.scaledW;
|
|
219
|
+
const h = this.scaledH;
|
|
220
|
+
const saliency = this.saliencyBuffer;
|
|
221
|
+
const peaks = [];
|
|
222
|
+
const r = this.suppressionRadius;
|
|
223
|
+
const r2 = r * r;
|
|
224
|
+
|
|
225
|
+
// Find all local maxima
|
|
226
|
+
const candidates = [];
|
|
227
|
+
for (let y = 1; y < h - 1; y++) {
|
|
228
|
+
for (let x = 1; x < w - 1; x++) {
|
|
229
|
+
const idx = y * w + x;
|
|
230
|
+
const val = saliency[idx];
|
|
231
|
+
|
|
232
|
+
// Check if local maximum (8-connected)
|
|
233
|
+
if (val > saliency[(y - 1) * w + (x - 1)] &&
|
|
234
|
+
val > saliency[(y - 1) * w + x] &&
|
|
235
|
+
val > saliency[(y - 1) * w + (x + 1)] &&
|
|
236
|
+
val > saliency[y * w + (x - 1)] &&
|
|
237
|
+
val > saliency[y * w + (x + 1)] &&
|
|
238
|
+
val > saliency[(y + 1) * w + (x - 1)] &&
|
|
239
|
+
val > saliency[(y + 1) * w + x] &&
|
|
240
|
+
val > saliency[(y + 1) * w + (x + 1)]) {
|
|
241
|
+
candidates.push({ x, y, value: val });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Sort by value descending
|
|
247
|
+
candidates.sort((a, b) => b.value - a.value);
|
|
248
|
+
|
|
249
|
+
// Non-maximum suppression
|
|
250
|
+
for (const cand of candidates) {
|
|
251
|
+
if (peaks.length >= this.maxPeaks) break;
|
|
252
|
+
|
|
253
|
+
// Check if too close to existing peaks
|
|
254
|
+
let suppress = false;
|
|
255
|
+
for (const peak of peaks) {
|
|
256
|
+
const dx = cand.x - peak.x;
|
|
257
|
+
const dy = cand.y - peak.y;
|
|
258
|
+
if (dx * dx + dy * dy < r2) {
|
|
259
|
+
suppress = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!suppress) {
|
|
265
|
+
// Convert to original image coordinates
|
|
266
|
+
peaks.push({
|
|
267
|
+
x: (cand.x + 0.5) * this.scale,
|
|
268
|
+
y: (cand.y + 0.5) * this.scale,
|
|
269
|
+
value: cand.value,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return peaks;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get saliency value at a specific location
|
|
279
|
+
*
|
|
280
|
+
* @param {number} x - X coordinate in original image
|
|
281
|
+
* @param {number} y - Y coordinate in original image
|
|
282
|
+
* @returns {number} Saliency value (0-1)
|
|
283
|
+
*/
|
|
284
|
+
getSaliencyAt(x, y) {
|
|
285
|
+
const sx = Math.floor(x / this.scale);
|
|
286
|
+
const sy = Math.floor(y / this.scale);
|
|
287
|
+
|
|
288
|
+
if (sx < 0 || sx >= this.scaledW || sy < 0 || sy >= this.scaledH) {
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return this.saliencyBuffer[sy * this.scaledW + sx];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { SaliencyMap };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scale Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Manages which octaves should be processed based on the current tracking state.
|
|
5
|
+
* Implements temporal consistency and interleave strategies to optimize performance.
|
|
6
|
+
*/
|
|
7
|
+
export class ScaleOrchestrator {
|
|
8
|
+
constructor(numOctaves, options = {}) {
|
|
9
|
+
this.numOctaves = numOctaves;
|
|
10
|
+
this.options = {
|
|
11
|
+
interleaveInterval: 10,
|
|
12
|
+
hysteresis: 1, // Number of adjacent octaves to keep
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
this.frameCount = 0;
|
|
17
|
+
this.lastActiveOctave = -1;
|
|
18
|
+
this.interleaveOctave = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Determine which octaves should be processed in the current frame
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} trackingState - Current state of tracking
|
|
25
|
+
* @returns {number[]} Array of octave indices to process
|
|
26
|
+
*/
|
|
27
|
+
getOctavesToProcess(trackingState = null) {
|
|
28
|
+
this.frameCount++;
|
|
29
|
+
|
|
30
|
+
// Case 1: No tracking or lost tracking -> Process all octaves
|
|
31
|
+
if (!trackingState || !trackingState.isTracking || trackingState.activeOctave === undefined) {
|
|
32
|
+
this.lastActiveOctave = -1;
|
|
33
|
+
return Array.from({ length: this.numOctaves }, (_, i) => i);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const activeScale = trackingState.activeOctave;
|
|
37
|
+
this.lastActiveOctave = activeScale;
|
|
38
|
+
|
|
39
|
+
// Case 2: Active tracking -> Focus on current scale and neighbors
|
|
40
|
+
const octaves = new Set();
|
|
41
|
+
|
|
42
|
+
// Add current and adjacent scales (Hysteresis)
|
|
43
|
+
for (let i = -this.options.hysteresis; i <= this.options.hysteresis; i++) {
|
|
44
|
+
const octave = activeScale + i;
|
|
45
|
+
if (octave >= 0 && octave < this.numOctaves) {
|
|
46
|
+
octaves.add(octave);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Case 3: Interleave - Periodically check a distant octave to ensure we don't "drift"
|
|
51
|
+
if (this.frameCount % this.options.interleaveInterval === 0) {
|
|
52
|
+
this.interleaveOctave = (this.interleaveOctave + 1) % this.numOctaves;
|
|
53
|
+
// If the interleave octave is already being processed, pick the next one
|
|
54
|
+
if (octaves.has(this.interleaveOctave)) {
|
|
55
|
+
this.interleaveOctave = (this.interleaveOctave + 1) % this.numOctaves;
|
|
56
|
+
}
|
|
57
|
+
octaves.add(this.interleaveOctave);
|
|
58
|
+
|
|
59
|
+
if (this.options.debug) {
|
|
60
|
+
console.log(`[ScaleOrchestrator] Interleave check on octave ${this.interleaveOctave}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = Array.from(octaves).sort((a, b) => a - b);
|
|
65
|
+
|
|
66
|
+
if (this.options.debug) {
|
|
67
|
+
console.log(`[ScaleOrchestrator] Active: ${activeScale}, Processing: [${result.join(', ')}]`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reset orchestrator state
|
|
75
|
+
*/
|
|
76
|
+
reset() {
|
|
77
|
+
this.frameCount = 0;
|
|
78
|
+
this.lastActiveOctave = -1;
|
|
79
|
+
}
|
|
80
|
+
}
|