@srsergio/taptapp-ar 1.0.2 → 1.0.3
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 +47 -45
- package/dist/compiler/aframe.js +0 -3
- package/dist/compiler/compiler-base.d.ts +3 -7
- package/dist/compiler/compiler-base.js +28 -14
- package/dist/compiler/compiler.js +1 -1
- package/dist/compiler/compiler.worker.js +1 -1
- package/dist/compiler/controller.js +4 -5
- package/dist/compiler/controller.worker.js +0 -2
- package/dist/compiler/detector/crop-detector.js +0 -2
- package/dist/compiler/detector/detector-lite.d.ts +73 -0
- package/dist/compiler/detector/detector-lite.js +430 -0
- package/dist/compiler/detector/detector.js +236 -243
- package/dist/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
- package/dist/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
- package/dist/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -18
- package/dist/compiler/detector/kernels/cpu/fakeShader.js +1 -1
- package/dist/compiler/detector/kernels/cpu/prune.d.ts +7 -1
- package/dist/compiler/detector/kernels/cpu/prune.js +1 -42
- package/dist/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
- package/dist/compiler/estimation/refine-estimate.js +0 -1
- package/dist/compiler/estimation/utils.d.ts +1 -1
- package/dist/compiler/estimation/utils.js +1 -14
- package/dist/compiler/image-list.js +4 -4
- package/dist/compiler/input-loader.js +2 -2
- package/dist/compiler/matching/hamming-distance.js +13 -13
- package/dist/compiler/matching/hierarchical-clustering.js +1 -1
- package/dist/compiler/matching/matching.d.ts +20 -4
- package/dist/compiler/matching/matching.js +67 -41
- package/dist/compiler/matching/ransacHomography.js +1 -2
- package/dist/compiler/node-worker.d.ts +1 -0
- package/dist/compiler/node-worker.js +84 -0
- package/dist/compiler/offline-compiler.d.ts +171 -6
- package/dist/compiler/offline-compiler.js +303 -421
- package/dist/compiler/tensorflow-setup.js +27 -1
- package/dist/compiler/three.js +3 -5
- package/dist/compiler/tracker/extract.d.ts +1 -0
- package/dist/compiler/tracker/extract.js +200 -244
- package/dist/compiler/tracker/tracker.d.ts +1 -1
- package/dist/compiler/tracker/tracker.js +13 -18
- package/dist/compiler/utils/cumsum.d.ts +4 -2
- package/dist/compiler/utils/cumsum.js +17 -19
- package/dist/compiler/utils/gpu-compute.d.ts +57 -0
- package/dist/compiler/utils/gpu-compute.js +262 -0
- package/dist/compiler/utils/images.d.ts +4 -4
- package/dist/compiler/utils/images.js +67 -53
- package/dist/compiler/utils/worker-pool.d.ts +14 -0
- package/dist/compiler/utils/worker-pool.js +84 -0
- package/package.json +11 -13
- package/src/compiler/aframe.js +2 -4
- package/src/compiler/compiler-base.js +29 -14
- package/src/compiler/compiler.js +1 -1
- package/src/compiler/compiler.worker.js +1 -1
- package/src/compiler/controller.js +4 -5
- package/src/compiler/controller.worker.js +0 -2
- package/src/compiler/detector/crop-detector.js +0 -2
- package/src/compiler/detector/detector-lite.js +494 -0
- package/src/compiler/detector/detector.js +1052 -1063
- package/src/compiler/detector/kernels/cpu/binomialFilter.js +0 -1
- package/src/compiler/detector/kernels/cpu/computeLocalization.js +0 -4
- package/src/compiler/detector/kernels/cpu/computeOrientationHistograms.js +0 -17
- package/src/compiler/detector/kernels/cpu/fakeShader.js +1 -1
- package/src/compiler/detector/kernels/cpu/prune.js +1 -37
- package/src/compiler/detector/kernels/webgl/upsampleBilinear.js +2 -2
- package/src/compiler/estimation/refine-estimate.js +0 -1
- package/src/compiler/estimation/utils.js +9 -24
- package/src/compiler/image-list.js +4 -4
- package/src/compiler/input-loader.js +2 -2
- package/src/compiler/matching/hamming-distance.js +11 -15
- package/src/compiler/matching/hierarchical-clustering.js +1 -1
- package/src/compiler/matching/matching.js +72 -42
- package/src/compiler/matching/ransacHomography.js +0 -2
- package/src/compiler/node-worker.js +93 -0
- package/src/compiler/offline-compiler.js +339 -504
- package/src/compiler/tensorflow-setup.js +29 -1
- package/src/compiler/three.js +3 -5
- package/src/compiler/tracker/extract.js +211 -267
- package/src/compiler/tracker/tracker.js +13 -22
- package/src/compiler/utils/cumsum.js +17 -19
- package/src/compiler/utils/gpu-compute.js +303 -0
- package/src/compiler/utils/images.js +84 -53
- package/src/compiler/utils/worker-pool.js +89 -0
- package/src/compiler/estimation/esimate-experiment.js +0 -316
- package/src/compiler/estimation/refine-estimate-experiment.js +0 -512
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detector Lite - Pure JavaScript Feature Detector
|
|
3
|
+
*
|
|
4
|
+
* Un detector de características simplificado que no depende de TensorFlow.
|
|
5
|
+
* Optimizado para velocidad en compilación offline.
|
|
6
|
+
*
|
|
7
|
+
* Implementa:
|
|
8
|
+
* - Construcción de pirámide gaussiana (con aceleración GPU opcional)
|
|
9
|
+
* - Diferencia de Gaussianas (DoG) para detección de extremos
|
|
10
|
+
* - Descriptores FREAK simplificados
|
|
11
|
+
*/
|
|
12
|
+
import { FREAKPOINTS } from "./freak.js";
|
|
13
|
+
import { gpuCompute } from "../utils/gpu-compute.js";
|
|
14
|
+
const PYRAMID_MIN_SIZE = 8;
|
|
15
|
+
const PYRAMID_MAX_OCTAVE = 5;
|
|
16
|
+
const NUM_BUCKETS_PER_DIMENSION = 8;
|
|
17
|
+
const MAX_FEATURES_PER_BUCKET = 3; // Optimizado: Reducido de 5 a 3 para menor peso
|
|
18
|
+
const ORIENTATION_NUM_BINS = 36;
|
|
19
|
+
const FREAK_EXPANSION_FACTOR = 7.0;
|
|
20
|
+
// Global GPU mode flag
|
|
21
|
+
let globalUseGPU = true;
|
|
22
|
+
/**
|
|
23
|
+
* Set global GPU mode for all DetectorLite instances
|
|
24
|
+
* @param {boolean} enabled - Whether to use GPU acceleration
|
|
25
|
+
*/
|
|
26
|
+
export const setDetectorGPUMode = (enabled) => {
|
|
27
|
+
globalUseGPU = enabled;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Detector de características sin TensorFlow
|
|
31
|
+
*/
|
|
32
|
+
export class DetectorLite {
|
|
33
|
+
constructor(width, height, options = {}) {
|
|
34
|
+
this.width = width;
|
|
35
|
+
this.height = height;
|
|
36
|
+
this.useGPU = options.useGPU !== undefined ? options.useGPU : globalUseGPU;
|
|
37
|
+
let numOctaves = 0;
|
|
38
|
+
let w = width, h = height;
|
|
39
|
+
while (w >= PYRAMID_MIN_SIZE && h >= PYRAMID_MIN_SIZE) {
|
|
40
|
+
w = Math.floor(w / 2);
|
|
41
|
+
h = Math.floor(h / 2);
|
|
42
|
+
numOctaves++;
|
|
43
|
+
if (numOctaves === PYRAMID_MAX_OCTAVE)
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
this.numOctaves = numOctaves;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Detecta características en una imagen en escala de grises
|
|
50
|
+
* @param {Float32Array|Uint8Array} imageData - Datos de imagen (width * height)
|
|
51
|
+
* @returns {{featurePoints: Array}} Puntos de características detectados
|
|
52
|
+
*/
|
|
53
|
+
detect(imageData) {
|
|
54
|
+
// Normalizar a Float32Array si es necesario
|
|
55
|
+
let data;
|
|
56
|
+
if (imageData instanceof Float32Array) {
|
|
57
|
+
data = imageData;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
data = new Float32Array(imageData.length);
|
|
61
|
+
for (let i = 0; i < imageData.length; i++) {
|
|
62
|
+
data[i] = imageData[i];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 1. Construir pirámide gaussiana
|
|
66
|
+
const pyramidImages = this._buildGaussianPyramid(data, this.width, this.height);
|
|
67
|
+
// 2. Construir pirámide DoG (Difference of Gaussians)
|
|
68
|
+
const dogPyramid = this._buildDogPyramid(pyramidImages);
|
|
69
|
+
// 3. Encontrar extremos locales
|
|
70
|
+
const extremas = this._findExtremas(dogPyramid, pyramidImages);
|
|
71
|
+
// 4. Aplicar pruning por buckets
|
|
72
|
+
const prunedExtremas = this._applyPrune(extremas);
|
|
73
|
+
// 5. Calcular orientaciones
|
|
74
|
+
this._computeOrientations(prunedExtremas, pyramidImages);
|
|
75
|
+
// 6. Calcular descriptores FREAK
|
|
76
|
+
this._computeFreakDescriptors(prunedExtremas, pyramidImages);
|
|
77
|
+
// Convertir a formato de salida
|
|
78
|
+
const featurePoints = prunedExtremas.map(ext => ({
|
|
79
|
+
maxima: ext.score > 0,
|
|
80
|
+
x: ext.x * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
|
|
81
|
+
y: ext.y * Math.pow(2, ext.octave) + Math.pow(2, ext.octave - 1) - 0.5,
|
|
82
|
+
scale: Math.pow(2, ext.octave),
|
|
83
|
+
angle: ext.angle || 0,
|
|
84
|
+
descriptors: ext.descriptors || []
|
|
85
|
+
}));
|
|
86
|
+
return { featurePoints };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Construye una pirámide gaussiana
|
|
90
|
+
*/
|
|
91
|
+
_buildGaussianPyramid(data, width, height) {
|
|
92
|
+
// Use GPU-accelerated pyramid if available
|
|
93
|
+
if (this.useGPU) {
|
|
94
|
+
try {
|
|
95
|
+
const gpuPyramid = gpuCompute.buildPyramid(data, width, height, this.numOctaves);
|
|
96
|
+
// Convert GPU pyramid format to expected format
|
|
97
|
+
const pyramid = [];
|
|
98
|
+
for (let i = 0; i < gpuPyramid.length && i < this.numOctaves; i++) {
|
|
99
|
+
const level = gpuPyramid[i];
|
|
100
|
+
// Apply second blur for DoG computation
|
|
101
|
+
const img2 = this._applyGaussianFilter(level.data, level.width, level.height);
|
|
102
|
+
pyramid.push([
|
|
103
|
+
{ data: level.data, width: level.width, height: level.height },
|
|
104
|
+
{ data: img2.data, width: level.width, height: level.height }
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
return pyramid;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
// Fall back to CPU if GPU fails
|
|
111
|
+
console.warn("GPU pyramid failed, falling back to CPU:", e.message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Original CPU implementation
|
|
115
|
+
const pyramid = [];
|
|
116
|
+
let currentData = data;
|
|
117
|
+
let currentWidth = width;
|
|
118
|
+
let currentHeight = height;
|
|
119
|
+
for (let i = 0; i < this.numOctaves; i++) {
|
|
120
|
+
const img1 = this._applyGaussianFilter(currentData, currentWidth, currentHeight);
|
|
121
|
+
const img2 = this._applyGaussianFilter(img1.data, currentWidth, currentHeight);
|
|
122
|
+
pyramid.push([
|
|
123
|
+
{ data: img1.data, width: currentWidth, height: currentHeight },
|
|
124
|
+
{ data: img2.data, width: currentWidth, height: currentHeight }
|
|
125
|
+
]);
|
|
126
|
+
// Downsample para siguiente octava
|
|
127
|
+
if (i < this.numOctaves - 1) {
|
|
128
|
+
const downsampled = this._downsample(img2.data, currentWidth, currentHeight);
|
|
129
|
+
currentData = downsampled.data;
|
|
130
|
+
currentWidth = downsampled.width;
|
|
131
|
+
currentHeight = downsampled.height;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return pyramid;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Aplica un filtro gaussiano binomial [1,4,6,4,1] - Optimizado
|
|
138
|
+
*/
|
|
139
|
+
_applyGaussianFilter(data, width, height) {
|
|
140
|
+
const output = new Float32Array(width * height);
|
|
141
|
+
const temp = new Float32Array(width * height);
|
|
142
|
+
const k0 = 1 / 16, k1 = 4 / 16, k2 = 6 / 16;
|
|
143
|
+
const w1 = width - 1;
|
|
144
|
+
const h1 = height - 1;
|
|
145
|
+
// Horizontal pass - unrolled kernel
|
|
146
|
+
for (let y = 0; y < height; y++) {
|
|
147
|
+
const rowOffset = y * width;
|
|
148
|
+
for (let x = 0; x < width; x++) {
|
|
149
|
+
const x0 = x < 2 ? 0 : x - 2;
|
|
150
|
+
const x1 = x < 1 ? 0 : x - 1;
|
|
151
|
+
const x3 = x > w1 - 1 ? w1 : x + 1;
|
|
152
|
+
const x4 = x > w1 - 2 ? w1 : x + 2;
|
|
153
|
+
temp[rowOffset + x] =
|
|
154
|
+
data[rowOffset + x0] * k0 +
|
|
155
|
+
data[rowOffset + x1] * k1 +
|
|
156
|
+
data[rowOffset + x] * k2 +
|
|
157
|
+
data[rowOffset + x3] * k1 +
|
|
158
|
+
data[rowOffset + x4] * k0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Vertical pass - unrolled kernel
|
|
162
|
+
for (let y = 0; y < height; y++) {
|
|
163
|
+
const y0 = (y < 2 ? 0 : y - 2) * width;
|
|
164
|
+
const y1 = (y < 1 ? 0 : y - 1) * width;
|
|
165
|
+
const y2 = y * width;
|
|
166
|
+
const y3 = (y > h1 - 1 ? h1 : y + 1) * width;
|
|
167
|
+
const y4 = (y > h1 - 2 ? h1 : y + 2) * width;
|
|
168
|
+
for (let x = 0; x < width; x++) {
|
|
169
|
+
output[y2 + x] =
|
|
170
|
+
temp[y0 + x] * k0 +
|
|
171
|
+
temp[y1 + x] * k1 +
|
|
172
|
+
temp[y2 + x] * k2 +
|
|
173
|
+
temp[y3 + x] * k1 +
|
|
174
|
+
temp[y4 + x] * k0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { data: output, width, height };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Downsample imagen por factor de 2
|
|
181
|
+
*/
|
|
182
|
+
_downsample(data, width, height) {
|
|
183
|
+
const newWidth = Math.floor(width / 2);
|
|
184
|
+
const newHeight = Math.floor(height / 2);
|
|
185
|
+
const output = new Float32Array(newWidth * newHeight);
|
|
186
|
+
for (let y = 0; y < newHeight; y++) {
|
|
187
|
+
for (let x = 0; x < newWidth; x++) {
|
|
188
|
+
// Interpolación bilinear
|
|
189
|
+
const srcX = x * 2 + 0.5;
|
|
190
|
+
const srcY = y * 2 + 0.5;
|
|
191
|
+
const x0 = Math.floor(srcX);
|
|
192
|
+
const y0 = Math.floor(srcY);
|
|
193
|
+
const x1 = Math.min(x0 + 1, width - 1);
|
|
194
|
+
const y1 = Math.min(y0 + 1, height - 1);
|
|
195
|
+
const fx = srcX - x0;
|
|
196
|
+
const fy = srcY - y0;
|
|
197
|
+
const v00 = data[y0 * width + x0];
|
|
198
|
+
const v10 = data[y0 * width + x1];
|
|
199
|
+
const v01 = data[y1 * width + x0];
|
|
200
|
+
const v11 = data[y1 * width + x1];
|
|
201
|
+
output[y * newWidth + x] =
|
|
202
|
+
v00 * (1 - fx) * (1 - fy) +
|
|
203
|
+
v10 * fx * (1 - fy) +
|
|
204
|
+
v01 * (1 - fx) * fy +
|
|
205
|
+
v11 * fx * fy;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { data: output, width: newWidth, height: newHeight };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Construye pirámide de diferencia de gaussianas
|
|
212
|
+
*/
|
|
213
|
+
_buildDogPyramid(pyramidImages) {
|
|
214
|
+
const dogPyramid = [];
|
|
215
|
+
for (let i = 0; i < pyramidImages.length; i++) {
|
|
216
|
+
const img1 = pyramidImages[i][0];
|
|
217
|
+
const img2 = pyramidImages[i][1];
|
|
218
|
+
const width = img1.width;
|
|
219
|
+
const height = img1.height;
|
|
220
|
+
const dog = new Float32Array(width * height);
|
|
221
|
+
for (let j = 0; j < dog.length; j++) {
|
|
222
|
+
dog[j] = img2.data[j] - img1.data[j];
|
|
223
|
+
}
|
|
224
|
+
dogPyramid.push({ data: dog, width, height });
|
|
225
|
+
}
|
|
226
|
+
return dogPyramid;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Encuentra extremos locales en la pirámide DoG
|
|
230
|
+
*/
|
|
231
|
+
_findExtremas(dogPyramid, pyramidImages) {
|
|
232
|
+
const extremas = [];
|
|
233
|
+
for (let octave = 1; octave < dogPyramid.length - 1; octave++) {
|
|
234
|
+
const curr = dogPyramid[octave];
|
|
235
|
+
const prev = dogPyramid[octave - 1];
|
|
236
|
+
const next = dogPyramid[octave + 1];
|
|
237
|
+
const width = curr.width;
|
|
238
|
+
const height = curr.height;
|
|
239
|
+
const prevWidth = prev.width;
|
|
240
|
+
const nextWidth = next.width;
|
|
241
|
+
for (let y = 1; y < height - 1; y++) {
|
|
242
|
+
for (let x = 1; x < width - 1; x++) {
|
|
243
|
+
const val = curr.data[y * width + x];
|
|
244
|
+
if (Math.abs(val) < 0.015)
|
|
245
|
+
continue; // Threshold
|
|
246
|
+
let isMaxima = true;
|
|
247
|
+
let isMinima = true;
|
|
248
|
+
// Check 3x3 neighborhood in current scale
|
|
249
|
+
for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
|
|
250
|
+
for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
|
|
251
|
+
if (dx === 0 && dy === 0)
|
|
252
|
+
continue;
|
|
253
|
+
const neighbor = curr.data[(y + dy) * width + (x + dx)];
|
|
254
|
+
if (neighbor >= val)
|
|
255
|
+
isMaxima = false;
|
|
256
|
+
if (neighbor <= val)
|
|
257
|
+
isMinima = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Check previous scale (scaled coordinates)
|
|
261
|
+
if (isMaxima || isMinima) {
|
|
262
|
+
const px = Math.floor(x * 2);
|
|
263
|
+
const py = Math.floor(y * 2);
|
|
264
|
+
for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
|
|
265
|
+
for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
|
|
266
|
+
const xx = Math.max(0, Math.min(prevWidth - 1, px + dx));
|
|
267
|
+
const yy = Math.max(0, Math.min(prev.height - 1, py + dy));
|
|
268
|
+
const neighbor = prev.data[yy * prevWidth + xx];
|
|
269
|
+
if (neighbor >= val)
|
|
270
|
+
isMaxima = false;
|
|
271
|
+
if (neighbor <= val)
|
|
272
|
+
isMinima = false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Check next scale (scaled coordinates)
|
|
277
|
+
if (isMaxima || isMinima) {
|
|
278
|
+
const nx = Math.floor(x / 2);
|
|
279
|
+
const ny = Math.floor(y / 2);
|
|
280
|
+
for (let dy = -1; dy <= 1 && (isMaxima || isMinima); dy++) {
|
|
281
|
+
for (let dx = -1; dx <= 1 && (isMaxima || isMinima); dx++) {
|
|
282
|
+
const xx = Math.max(0, Math.min(nextWidth - 1, nx + dx));
|
|
283
|
+
const yy = Math.max(0, Math.min(next.height - 1, ny + dy));
|
|
284
|
+
const neighbor = next.data[yy * nextWidth + xx];
|
|
285
|
+
if (neighbor >= val)
|
|
286
|
+
isMaxima = false;
|
|
287
|
+
if (neighbor <= val)
|
|
288
|
+
isMinima = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (isMaxima || isMinima) {
|
|
293
|
+
extremas.push({
|
|
294
|
+
score: isMaxima ? Math.abs(val) : -Math.abs(val),
|
|
295
|
+
octave,
|
|
296
|
+
x,
|
|
297
|
+
y,
|
|
298
|
+
absScore: Math.abs(val)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return extremas;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Aplica pruning para mantener solo los mejores features por bucket
|
|
308
|
+
*/
|
|
309
|
+
_applyPrune(extremas) {
|
|
310
|
+
const nBuckets = NUM_BUCKETS_PER_DIMENSION;
|
|
311
|
+
const nFeatures = MAX_FEATURES_PER_BUCKET;
|
|
312
|
+
// Agrupar por buckets
|
|
313
|
+
const buckets = [];
|
|
314
|
+
for (let i = 0; i < nBuckets * nBuckets; i++) {
|
|
315
|
+
buckets.push([]);
|
|
316
|
+
}
|
|
317
|
+
for (const ext of extremas) {
|
|
318
|
+
const bucketX = Math.min(nBuckets - 1, Math.floor(ext.x / (this.width / Math.pow(2, ext.octave)) * nBuckets));
|
|
319
|
+
const bucketY = Math.min(nBuckets - 1, Math.floor(ext.y / (this.height / Math.pow(2, ext.octave)) * nBuckets));
|
|
320
|
+
const bucketIdx = bucketY * nBuckets + bucketX;
|
|
321
|
+
if (bucketIdx >= 0 && bucketIdx < buckets.length) {
|
|
322
|
+
buckets[bucketIdx].push(ext);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Seleccionar top features por bucket
|
|
326
|
+
const result = [];
|
|
327
|
+
for (const bucket of buckets) {
|
|
328
|
+
bucket.sort((a, b) => b.absScore - a.absScore);
|
|
329
|
+
for (let i = 0; i < Math.min(nFeatures, bucket.length); i++) {
|
|
330
|
+
result.push(bucket[i]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Calcula la orientación de cada feature
|
|
337
|
+
*/
|
|
338
|
+
_computeOrientations(extremas, pyramidImages) {
|
|
339
|
+
for (const ext of extremas) {
|
|
340
|
+
if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
|
|
341
|
+
ext.angle = 0;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const img = pyramidImages[ext.octave][1];
|
|
345
|
+
const width = img.width;
|
|
346
|
+
const height = img.height;
|
|
347
|
+
const data = img.data;
|
|
348
|
+
const x = Math.floor(ext.x);
|
|
349
|
+
const y = Math.floor(ext.y);
|
|
350
|
+
// Compute gradient histogram
|
|
351
|
+
const histogram = new Float32Array(ORIENTATION_NUM_BINS);
|
|
352
|
+
const radius = 4;
|
|
353
|
+
for (let dy = -radius; dy <= radius; dy++) {
|
|
354
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
355
|
+
const yy = y + dy;
|
|
356
|
+
const xx = x + dx;
|
|
357
|
+
if (yy <= 0 || yy >= height - 1 || xx <= 0 || xx >= width - 1)
|
|
358
|
+
continue;
|
|
359
|
+
const gradY = data[(yy + 1) * width + xx] - data[(yy - 1) * width + xx];
|
|
360
|
+
const gradX = data[yy * width + xx + 1] - data[yy * width + xx - 1];
|
|
361
|
+
const mag = Math.sqrt(gradX * gradX + gradY * gradY);
|
|
362
|
+
const angle = Math.atan2(gradY, gradX) + Math.PI; // 0 to 2*PI
|
|
363
|
+
const bin = Math.floor(angle / (2 * Math.PI) * ORIENTATION_NUM_BINS) % ORIENTATION_NUM_BINS;
|
|
364
|
+
const weight = Math.exp(-(dx * dx + dy * dy) / (2 * radius * radius));
|
|
365
|
+
histogram[bin] += mag * weight;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Find peak
|
|
369
|
+
let maxBin = 0;
|
|
370
|
+
for (let i = 1; i < ORIENTATION_NUM_BINS; i++) {
|
|
371
|
+
if (histogram[i] > histogram[maxBin]) {
|
|
372
|
+
maxBin = i;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
ext.angle = (maxBin + 0.5) * 2 * Math.PI / ORIENTATION_NUM_BINS - Math.PI;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Calcula descriptores FREAK
|
|
380
|
+
*/
|
|
381
|
+
_computeFreakDescriptors(extremas, pyramidImages) {
|
|
382
|
+
for (const ext of extremas) {
|
|
383
|
+
if (ext.octave < 1 || ext.octave >= pyramidImages.length) {
|
|
384
|
+
ext.descriptors = [];
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const img = pyramidImages[ext.octave][1];
|
|
388
|
+
const width = img.width;
|
|
389
|
+
const height = img.height;
|
|
390
|
+
const data = img.data;
|
|
391
|
+
const cos = Math.cos(ext.angle || 0) * FREAK_EXPANSION_FACTOR;
|
|
392
|
+
const sin = Math.sin(ext.angle || 0) * FREAK_EXPANSION_FACTOR;
|
|
393
|
+
// Sample FREAK points
|
|
394
|
+
const samples = new Float32Array(FREAKPOINTS.length);
|
|
395
|
+
for (let i = 0; i < FREAKPOINTS.length; i++) {
|
|
396
|
+
const [, fx, fy] = FREAKPOINTS[i];
|
|
397
|
+
const xp = ext.x + fx * cos - fy * sin;
|
|
398
|
+
const yp = ext.y + fx * sin + fy * cos;
|
|
399
|
+
const x0 = Math.max(0, Math.min(width - 2, Math.floor(xp)));
|
|
400
|
+
const y0 = Math.max(0, Math.min(height - 2, Math.floor(yp)));
|
|
401
|
+
const x1 = x0 + 1;
|
|
402
|
+
const y1 = y0 + 1;
|
|
403
|
+
const fracX = xp - x0;
|
|
404
|
+
const fracY = yp - y0;
|
|
405
|
+
samples[i] =
|
|
406
|
+
data[y0 * width + x0] * (1 - fracX) * (1 - fracY) +
|
|
407
|
+
data[y0 * width + x1] * fracX * (1 - fracY) +
|
|
408
|
+
data[y1 * width + x0] * (1 - fracX) * fracY +
|
|
409
|
+
data[y1 * width + x1] * fracX * fracY;
|
|
410
|
+
}
|
|
411
|
+
// Pack pairs into Uint8Array (84 bytes per descriptor)
|
|
412
|
+
const descriptor = new Uint8Array(84);
|
|
413
|
+
let bitCount = 0;
|
|
414
|
+
let byteIdx = 0;
|
|
415
|
+
for (let i = 0; i < FREAKPOINTS.length; i++) {
|
|
416
|
+
for (let j = i + 1; j < FREAKPOINTS.length; j++) {
|
|
417
|
+
if (samples[i] < samples[j]) {
|
|
418
|
+
descriptor[byteIdx] |= (1 << (7 - bitCount));
|
|
419
|
+
}
|
|
420
|
+
bitCount++;
|
|
421
|
+
if (bitCount === 8) {
|
|
422
|
+
byteIdx++;
|
|
423
|
+
bitCount = 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
ext.descriptors = descriptor;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|