Преглед изворни кода

Introduce ImageTrackerUtils

customisations
alemart пре 10 месеци
родитељ
комит
3f2df05f43
1 измењених фајлова са 461 додато и 0 уклоњено
  1. 461
    0
      src/trackers/image-tracker/image-tracker-utils.ts

+ 461
- 0
src/trackers/image-tracker/image-tracker-utils.ts Прегледај датотеку

@@ -0,0 +1,461 @@
1
+/*
2
+ * encantar.js
3
+ * GPU-accelerated Augmented Reality for the web
4
+ * Copyright (C) 2022-2024 Alexandre Martins <alemartf(at)gmail.com>
5
+ *
6
+ * This program is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Lesser General Public License as published
8
+ * by the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * This program is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
+ * GNU Lesser General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Lesser General Public License
17
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
+ *
19
+ * image-tracker-utils.ts
20
+ * Image Tracker: Utilities
21
+ */
22
+
23
+import Speedy from 'speedy-vision';
24
+import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
25
+import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
26
+import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
27
+import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
28
+import { SpeedyPoint2 } from 'speedy-vision/types/core/speedy-point';
29
+import { SpeedyVector2 } from 'speedy-vision/types/core/speedy-vector';
30
+import { SpeedyKeypoint } from 'speedy-vision/types/core/speedy-keypoint';
31
+import { ImageTracker } from './image-tracker';
32
+import { ReferenceImage } from './reference-image';
33
+import { Utils } from '../../utils/utils';
34
+import { IllegalOperationError, IllegalArgumentError, NumericalError } from '../../utils/errors';
35
+import { NIS_SIZE, TRACK_GRID_GRANULARITY } from './settings';
36
+
37
+/*
38
+
39
+Definitions:
40
+------------
41
+
42
+1. Raster space:
43
+   an image space whose top-left coordinate is (0,0) and whose bottom-right
44
+   coordinate is (w-1,h-1), where (w,h) is its size. The y-axis points down.
45
+
46
+2. AR screen size:
47
+   size in pixels used for image processing operations. It's determined by the
48
+   resolution of the tracker and by the aspect ratio of the input media.
49
+
50
+3. AR screen space (screen):
51
+   a raster space whose size is the AR screen size.
52
+
53
+4. Normalized Image Space (NIS):
54
+   a raster space whose size is N x N, where N = NIS_SIZE.
55
+
56
+5. Normalized Device Coordinates (NDC):
57
+   the normalized 2D space [-1,1]x[-1,1]. The origin is at the center and the
58
+   y-axis points up.
59
+
60
+*/
61
+
62
+/** An ordered pair [src, dest] of keypoints */
63
+export type ImageTrackerKeypointPair = [ Readonly<SpeedyKeypoint>, Readonly<SpeedyKeypoint> ];
64
+
65
+/**
66
+ * Utilities for the Image Tracker
67
+ */
68
+export class ImageTrackerUtils
69
+{
70
+    /**
71
+     * Find a transformation that converts a raster space to NIS
72
+     * @param size size of the raster space
73
+     * @returns a 3x3 matrix
74
+     */
75
+    static rasterToNIS(size: SpeedySize): SpeedyMatrix
76
+    {
77
+        const sx = NIS_SIZE / size.width;
78
+        const sy = NIS_SIZE / size.height;
79
+
80
+        return Speedy.Matrix(3, 3, [
81
+            sx, 0,  0,
82
+            0,  sy, 0,
83
+            0,  0,  1
84
+        ]);
85
+    }
86
+
87
+    /**
88
+     * Find a transformation that converts a raster space to NDC
89
+     * @param size size of the raster space
90
+     * @returns a 3x3 matrix
91
+     */
92
+    static rasterToNDC(size: SpeedySize): SpeedyMatrix
93
+    {
94
+        const w = size.width, h = size.height;
95
+
96
+        return Speedy.Matrix(3, 3, [
97
+            2/w, 0,   0,
98
+            0,  -2/h, 0,
99
+           -1,   1,   1
100
+        ]);
101
+    }
102
+
103
+    /**
104
+     * Find a transformation that converts NDC to a raster space
105
+     * @param size size of the raster space
106
+     * @returns a 3x3 matrix
107
+     */
108
+    static NDCToRaster(size: SpeedySize): SpeedyMatrix
109
+    {
110
+        const w = size.width, h = size.height;
111
+
112
+        return Speedy.Matrix(3, 3, [
113
+            w/2, 0,   0,
114
+            0,  -h/2, 0,
115
+            w/2, h/2, 1
116
+        ]);
117
+    }
118
+
119
+    /**
120
+     * Find a transformation that scales points in NDC
121
+     * @param sx horizontal scale factor
122
+     * @param sy vertical scale factor
123
+     * @returns a 3x3 matrix
124
+     */
125
+    static scaleNDC(sx: number, sy: number = sx): SpeedyMatrix
126
+    {
127
+        // In NDC, the origin is at the center of the space!
128
+        return Speedy.Matrix(3, 3, [
129
+            sx, 0,  0,
130
+            0,  sy, 0,
131
+            0,  0,  1
132
+        ]);
133
+    }
134
+
135
+    /**
136
+     * Find a scale transformation in NDC such that the output has a desired aspect ratio
137
+     * @param aspectRatio desired aspect ratio
138
+     * @param scale optional scale factor in both axes
139
+     * @returns a 3x3 matrix
140
+     */
141
+    static bestFitScaleNDC(aspectRatio: number, scale: number = 1): SpeedyMatrix
142
+    {
143
+        if(aspectRatio >= 1)
144
+            return this.scaleNDC(scale, scale / aspectRatio); // s/(s/a) = a, sx >= sy
145
+        else
146
+            return this.scaleNDC(scale * aspectRatio, scale); // (s*a)/s = a, sx < sy
147
+    }
148
+
149
+    /**
150
+     * Find the inverse matrix of bestFitScaleNDC()
151
+     * @param aspectRatio as given to bestFitScaleNDC()
152
+     * @param scale optional, as given to bestFitScaleNDC()
153
+     * @returns a 3x3 matrix
154
+     */
155
+    static inverseBestFitScaleNDC(aspectRatio: number, scale: number = 1): SpeedyMatrix
156
+    {
157
+        if(aspectRatio >= 1)
158
+            return this.scaleNDC(1 / scale, aspectRatio / scale);
159
+        else
160
+            return this.scaleNDC(1 / (scale * aspectRatio), 1 / scale);
161
+    }
162
+
163
+    /**
164
+     * Find the best-fit aspect ratio for the rectification of the reference image in NDC
165
+     * @param imageTracker
166
+     * @param referenceImage
167
+     * @returns a best-fit aspect ratio
168
+     */
169
+    static bestFitAspectRatioNDC(imageTracker: ImageTracker, referenceImage: ReferenceImage): number
170
+    {
171
+        const screenSize = imageTracker.screenSize;
172
+        const screenAspectRatio = screenSize.width / screenSize.height;
173
+        const referenceImageMedia = imageTracker.database._findMedia(referenceImage.name);
174
+        const referenceImageAspectRatio = referenceImageMedia.size.width / referenceImageMedia.size.height;
175
+
176
+        /*
177
+        
178
+        The best-fit aspectRatio (a) is constructed as follows:
179
+
180
+        1) a fully stretched(*) and distorted reference image in NDC:
181
+           a = 1
182
+
183
+        2) a square in NDC:
184
+           a = 1 / screenAspectRatio
185
+
186
+        3) an image with the aspect ratio of the reference image in NDC:
187
+           a = referenceImageAspectRatio * (1 / screenAspectRatio)
188
+
189
+        (*) AR screen space
190
+
191
+        By transforming the reference image twice, first by converting it to AR
192
+        screen space, and then by rectifying it, we lose a little bit of quality.
193
+        Nothing to be too concerned about, though?
194
+
195
+        */
196
+
197
+        return referenceImageAspectRatio / screenAspectRatio;
198
+    }
199
+
200
+    /**
201
+     * Given n > 0 pairs (src_i, dest_i) of keypoints in NIS,
202
+     * convert them to NDC and output a 2 x 2n matrix of the form:
203
+     * [ src_0.x  src_1.x  ... | dest_0.x  dest_1.x  ... ]
204
+     * [ src_0.y  src_1.y  ... | dest_0.y  dest_1.y  ... ]
205
+     * @param pairs pairs of keypoints in NIS
206
+     * @returns 2 x 2n matrix with two 2 x n blocks: [ src | dest ]
207
+     * @throws
208
+     */
209
+    static compilePairsOfKeypointsNDC(pairs: ImageTrackerKeypointPair[]): SpeedyMatrix
210
+    {
211
+        const n = pairs.length;
212
+
213
+        if(n == 0)
214
+            throw new IllegalArgumentError();
215
+
216
+        const scale = 2 / NIS_SIZE;
217
+        const data = new Array<number>(2 * 2*n);
218
+
219
+        for(let i = 0, j = 0, k = 2*n; i < n; i++, j += 2, k += 2) {
220
+            const src = pairs[i][0];
221
+            const dest = pairs[i][1];
222
+
223
+            data[j] = src.x * scale - 1; // convert from NIS to NDC
224
+            data[j+1] = 1 - src.y * scale; // flip y-axis
225
+
226
+            data[k] = dest.x * scale - 1;
227
+            data[k+1] = 1 - dest.y * scale;
228
+        }
229
+
230
+        return Speedy.Matrix(2, 2*n, data);
231
+    }
232
+
233
+    /**
234
+     * Given n > 0 pairs of keypoints in NDC as a 2 x 2n [ src | dest ] matrix,
235
+     * find a perspective warp (homography) from src to dest in NDC
236
+     * @param points compiled pairs of keypoints in NDC
237
+     * @param options to be passed to speedy-vision
238
+     * @returns a pair [ 3x3 transformation matrix, quality score ]
239
+     */
240
+    static findPerspectiveWarpNDC(points: SpeedyMatrix, options: object): SpeedyPromise<[SpeedyMatrix,number]>
241
+    {
242
+        // too few data points?
243
+        const n = points.columns / 2;
244
+        if(n < 4) {
245
+            return Speedy.Promise.reject(
246
+                new IllegalArgumentError(`Too few data points to compute a perspective warp`)
247
+            );
248
+        }
249
+
250
+        // compute a homography
251
+        const src = points.block(0, 1, 0, n-1);
252
+        const dest = points.block(0, 1, n, 2*n-1);
253
+        const mask = Speedy.Matrix.Zeros(1, n);
254
+
255
+        return Speedy.Matrix.findHomography(
256
+            Speedy.Matrix.Zeros(3),
257
+            src,
258
+            dest,
259
+            Object.assign({ mask }, options)
260
+        ).then(homography => {
261
+
262
+            // check if this is a valid warp
263
+            const a00 = homography.at(0,0);
264
+            if(Number.isNaN(a00))
265
+                throw new NumericalError(`Can't compute a perspective warp: bad keypoints`);
266
+
267
+            // count the number of inliers
268
+            const inliers = mask.read();
269
+            let inlierCount = 0;
270
+            for(let i = inliers.length - 1; i >= 0; i--)
271
+                inlierCount += inliers[i];
272
+            const score = inlierCount / inliers.length;
273
+
274
+            // done!
275
+            return [ homography, score ];
276
+
277
+        });
278
+    }
279
+
280
+    /**
281
+     * Given n > 0 pairs of keypoints in NDC as a 2 x 2n [ src | dest ] matrix,
282
+     * find an affine warp from src to dest in NDC. The affine warp is given as
283
+     * a 3x3 matrix whose last row is [0 0 1]
284
+     * @param points compiled pairs of keypoints in NDC
285
+     * @param options to be passed to speedy-vision
286
+     * @returns a pair [ 3x3 transformation matrix, quality score ]
287
+     */
288
+    static findAffineWarpNDC(points: SpeedyMatrix, options: object): SpeedyPromise<[SpeedyMatrix,number]>
289
+    {
290
+        // too few data points?
291
+        const n = points.columns / 2;
292
+        if(n < 3) {
293
+            return Speedy.Promise.reject(
294
+                new IllegalArgumentError(`Too few data points to compute an affine warp`)
295
+            );
296
+        }
297
+
298
+        // compute an affine transformation
299
+        const model = Speedy.Matrix.Eye(3);
300
+        const src = points.block(0, 1, 0, n-1);
301
+        const dest = points.block(0, 1, n, 2*n-1);
302
+        const mask = Speedy.Matrix.Zeros(1, n);
303
+
304
+        return Speedy.Matrix.findAffineTransform(
305
+            model.block(0, 1, 0, 2), // 2x3 submatrix
306
+            src,
307
+            dest,
308
+            Object.assign({ mask }, options)
309
+        ).then(_ => {
310
+
311
+            // check if this is a valid warp
312
+            const a00 = model.at(0,0);
313
+            if(Number.isNaN(a00))
314
+                throw new NumericalError(`Can't compute an affine warp: bad keypoints`);
315
+
316
+            // count the number of inliers
317
+            const inliers = mask.read();
318
+            let inlierCount = 0;
319
+            for(let i = inliers.length - 1; i >= 0; i--)
320
+                inlierCount += inliers[i];
321
+            const score = inlierCount / inliers.length;
322
+
323
+            // done!
324
+            return [ model, score ];
325
+
326
+        });
327
+    }
328
+
329
+    /**
330
+     * Find a polyline in Normalized Device Coordinates (NDC)
331
+     * @param homography maps the corners of NDC to a quadrilateral in NDC
332
+     * @returns 4 points in NDC
333
+     */
334
+    static findPolylineNDC(homography: SpeedyMatrix): SpeedyPoint2[]
335
+    {
336
+        const h = homography.read();
337
+        const uv = [ -1, +1,    -1, -1,    +1, -1,    +1, +1 ]; // the corners of a reference image in NDC
338
+        const polyline = new Array<SpeedyPoint2>(4);
339
+
340
+        for(let i = 0, j = 0; i < 4; i++, j += 2) {
341
+            const u = uv[j], v = uv[j+1];
342
+
343
+            const x = h[0]*u + h[3]*v + h[6];
344
+            const y = h[1]*u + h[4]*v + h[7];
345
+            const w = h[2]*u + h[5]*v + h[8];
346
+
347
+            polyline[i] = Speedy.Point2(x/w, y/w);
348
+        }
349
+
350
+        return polyline;
351
+    }
352
+
353
+    /**
354
+     * Find a better spatial distribution of the input matches
355
+     * @param pairs in the [src, dest] format
356
+     * @returns refined pairs of quality matches
357
+     */
358
+    static refineMatchingPairs(pairs: ImageTrackerKeypointPair[]): ImageTrackerKeypointPair[]
359
+    {
360
+        // collect all keypoints obtained in this frame
361
+        const m = pairs.length;
362
+        const destKeypoints = new Array<SpeedyKeypoint>(m);
363
+
364
+        for(let j = 0; j < m; j++)
365
+            destKeypoints[j] = pairs[j][1];
366
+
367
+        // find a better spatial distribution of the keypoints
368
+        const indices = this._distributeKeypoints(destKeypoints);
369
+
370
+        // assemble output
371
+        const n = indices.length; // number of refined matches
372
+        const result = new Array<ImageTrackerKeypointPair>(n);
373
+
374
+        for(let i = 0; i < n; i++)
375
+            result[i] = pairs[indices[i]];
376
+
377
+        // done!
378
+        return result;
379
+    }
380
+
381
+    /**
382
+     * Spatially distribute keypoints over a grid
383
+     * @param keypoints keypoints to be distributed
384
+     * @returns a list of indices of keypoints[]
385
+     */
386
+    private static _distributeKeypoints(keypoints: SpeedyKeypoint[]): number[]
387
+    {
388
+        // create a grid
389
+        const gridCells = TRACK_GRID_GRANULARITY; // number of grid elements in each axis
390
+        const numberOfCells = gridCells * gridCells;
391
+        const n = keypoints.length;
392
+
393
+        // get the coordinates of the keypoints
394
+        const points: number[] = new Array(2 * n);
395
+        for(let i = 0, j = 0; i < n; i++, j += 2) {
396
+            points[j] = keypoints[i].x;
397
+            points[j+1] = keypoints[i].y;
398
+        }
399
+
400
+        // normalize the coordinates to [0,1) x [0,1)
401
+        this._normalizePoints(points);
402
+
403
+        // distribute the keypoints over the grid
404
+        const grid = new Array<number>(numberOfCells).fill(-1);
405
+        for(let i = 0, j = 0; i < n; i++, j += 2) {
406
+            // find the grid location of the i-th point
407
+            const xg = Math.floor(points[j] * gridCells); // 0 <= xg,yg < gridCells
408
+            const yg = Math.floor(points[j+1] * gridCells);
409
+
410
+            // store the index of the i-th point in the grid
411
+            const k = yg * gridCells + xg;
412
+            if(grid[k] < 0)
413
+                grid[k] = i;
414
+        }
415
+
416
+        // retrieve points of the grid
417
+        let m = 0;
418
+        const indices = new Array<number>(numberOfCells);
419
+        for(let g = 0; g < numberOfCells; g++) {
420
+            if(grid[g] >= 0)
421
+                indices[m++] = grid[g];
422
+        }
423
+        indices.length = m;
424
+
425
+        // done!
426
+        return indices;
427
+    }
428
+
429
+    /**
430
+     * Normalize points to [0,1)^2
431
+     * @param points 2 x n matrix of points in column-major format
432
+     * @returns points
433
+     */
434
+    private static _normalizePoints(points: number[]): number[]
435
+    {
436
+        Utils.assert(points.length % 2 == 0);
437
+
438
+        const n = points.length / 2;
439
+        if(n == 0)
440
+            return points;
441
+
442
+        let xmin = Number.POSITIVE_INFINITY, xmax = Number.NEGATIVE_INFINITY;
443
+        let ymin = Number.POSITIVE_INFINITY, ymax = Number.NEGATIVE_INFINITY;
444
+        for(let i = 0, j = 0; i < n; i++, j += 2) {
445
+            const x = points[j], y = points[j+1];
446
+            xmin = x < xmin ? x : xmin;
447
+            ymin = y < ymin ? y : ymin;
448
+            xmax = x > xmax ? x : xmax;
449
+            ymax = y > ymax ? y : ymax;
450
+        }
451
+
452
+        const xlen = xmax - xmin + 1; // +1 is a correction factor, so that 0 <= x,y < 1
453
+        const ylen = ymax - ymin + 1;
454
+        for(let i = 0, j = 0; i < n; i++, j += 2) {
455
+            points[j] = (points[j] - xmin) / xlen;
456
+            points[j+1] = (points[j+1] - ymin) / ylen;
457
+        }
458
+
459
+        return points;
460
+    }
461
+}

Loading…
Откажи
Сачувај