|
@@ -26,6 +26,7 @@ import { SpeedyVector2 } from 'speedy-vision/types/core/speedy-vector';
|
26
|
26
|
import { SpeedySize } from 'speedy-vision/types/core/speedy-size';
|
27
|
27
|
import { SpeedyMedia } from 'speedy-vision/types/core/speedy-media';
|
28
|
28
|
import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
|
|
29
|
+import { SpeedyMatrixExpr } from 'speedy-vision/types/core/speedy-matrix-expr';
|
29
|
30
|
import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
|
30
|
31
|
import { SpeedyPipeline, SpeedyPipelineOutput } from 'speedy-vision/types/core/pipeline/pipeline';
|
31
|
32
|
import { SpeedyPipelineNodeImageSource } from 'speedy-vision/types/core/pipeline/nodes/images/source';
|
|
@@ -39,40 +40,33 @@ import { SpeedyPipelineNodeKeypointBorderClipper } from 'speedy-vision/types/cor
|
39
|
40
|
import { SpeedyPipelineNodeKeypointTransformer } from 'speedy-vision/types/core/pipeline/nodes/keypoints/transformer';
|
40
|
41
|
import { SpeedyKeypoint, SpeedyTrackedKeypoint, SpeedyMatchedKeypoint } from 'speedy-vision/types/core/speedy-keypoint';
|
41
|
42
|
import { ImageTracker, ImageTrackerOutput, ImageTrackerStateName, ImageTrackerResult, TrackableImage } from '../image-tracker';
|
42
|
|
-import { ImageTrackerState, ImageTrackerStateOutput } from './state';
|
|
43
|
+import { ImageTrackerUtils, ImageTrackerKeypointPair } from '../image-tracker-utils';
|
43
|
44
|
import { ImageTrackerEvent } from '../image-tracker-event';
|
|
45
|
+import { ImageTrackerState, ImageTrackerStateOutput } from './state';
|
44
|
46
|
import { Nullable, Utils } from '../../../utils/utils';
|
45
|
47
|
import { ReferenceImage } from '../reference-image';
|
46
|
48
|
import { CameraModel } from '../../../geometry/camera-model';
|
47
|
49
|
import { Viewer } from '../../../geometry/viewer';
|
48
|
50
|
import { Pose } from '../../../geometry/pose';
|
49
|
51
|
import { Transform } from '../../../geometry/transform';
|
50
|
|
-import { IllegalOperationError, IllegalArgumentError, TrackingError } from '../../../utils/errors';
|
|
52
|
+import { IllegalOperationError, IllegalArgumentError, TrackingError, NumericalError } from '../../../utils/errors';
|
51
|
53
|
import {
|
52
|
|
- TRACK_RECTIFIED_BORDER, TRACK_CLIPPING_BORDER, TRACK_MIN_MATCHES, TRACK_LOST_TOLERANCE,
|
|
54
|
+ TRACK_RECTIFIED_SCALE, TRACK_CLIPPING_BORDER, TRACK_MIN_MATCHES, TRACK_LOST_TOLERANCE,
|
53
|
55
|
NIGHTVISION_GAIN, NIGHTVISION_OFFSET, NIGHTVISION_DECAY, TRACK_WITH_NIGHTVISION,
|
54
|
56
|
ORB_GAUSSIAN_KSIZE, ORB_GAUSSIAN_SIGMA,
|
55
|
57
|
SUBPIXEL_GAUSSIAN_KSIZE, SUBPIXEL_GAUSSIAN_SIGMA,
|
56
|
58
|
TRACK_HARRIS_QUALITY, TRACK_DETECTOR_CAPACITY, TRACK_MAX_KEYPOINTS,
|
57
|
|
- TRACK_RANSAC_REPROJECTIONERROR, TRACK_GRID_GRANULARITY, TRACK_MATCH_RATIO,
|
58
|
|
- NIGHTVISION_QUALITY,
|
59
|
|
- SUBPIXEL_METHOD,
|
|
59
|
+ TRACK_RANSAC_REPROJECTIONERROR_NDC, TRACK_MATCH_RATIO,
|
|
60
|
+ NIGHTVISION_QUALITY, SUBPIXEL_METHOD,
|
60
|
61
|
} from '../settings';
|
61
|
62
|
import { Settings } from '../../../core/settings';
|
62
|
63
|
|
63
|
|
-
|
64
|
64
|
/** Whether or not we want to accelerate GPU-CPU transfers. Using turbo costs a slight delay on the tracking */
|
65
|
65
|
const USE_TURBO = true;
|
66
|
66
|
|
67
|
67
|
/** Number of PBOs; meaningful only when using turbo */
|
68
|
68
|
const NUMBER_OF_PBOS = 2;
|
69
|
69
|
|
70
|
|
-/** Frame skipping; meaningful only when using turbo */
|
71
|
|
-const TURBO_SKIP = 2;
|
72
|
|
-
|
73
|
|
-/** A pair (a,b) of arrays of keypoints such that keypoint a[i] is a match to keypoint b[i] for all i */
|
74
|
|
-type QualityMatches = [ SpeedyMatchedKeypoint[], SpeedyKeypoint[] ];
|
75
|
|
-
|
76
|
70
|
|
77
|
71
|
|
78
|
72
|
/**
|
|
@@ -91,33 +85,29 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
91
|
85
|
/** current homography (for computing the pose) */
|
92
|
86
|
private _poseHomography: SpeedyMatrix;
|
93
|
87
|
|
94
|
|
- /** initial homography (i.e., the homography we found when we first started tracking) */
|
95
|
|
- private _initialHomography: SpeedyMatrix; // from (full)screen to the actual target
|
96
|
|
-
|
97
|
88
|
/** initial keypoints (i.e., the keypoints we found when we first started tracking) */
|
98
|
|
- private _initialKeypoints: SpeedyKeypoint[];
|
|
89
|
+ private _templateKeypoints: SpeedyKeypoint[];
|
99
|
90
|
|
100
|
|
- /** a helper */
|
101
|
|
- private _counter: number;
|
102
|
|
-
|
103
|
|
- /** camera model */
|
104
|
|
- private _camera: CameraModel;
|
|
91
|
+ /** the screen size when the tracking began */
|
|
92
|
+ private _initialScreenSize: SpeedySize;
|
105
|
93
|
|
106
|
|
- /** predicted keypoints */
|
107
|
|
- private _predictedKeypoints: SpeedyMatchedKeypoint[];
|
|
94
|
+ /** last output of the tracker */
|
|
95
|
+ private _lastOutput: ImageTrackerOutput;
|
108
|
96
|
|
109
|
|
- /** last pipeline output */
|
|
97
|
+ /** last output of the pipeline */
|
110
|
98
|
private _lastPipelineOutput: SpeedyPipelineOutput;
|
111
|
99
|
|
112
|
|
- /** a helper */
|
113
|
|
- private _pipelineCounter: number;
|
|
100
|
+ /** a helper for frame skipping */
|
|
101
|
+ private _skipCounter: number;
|
114
|
102
|
|
115
|
|
- /** last output */
|
116
|
|
- private _lastOutput: ImageTrackerOutput;
|
|
103
|
+ /** a helper */
|
|
104
|
+ private _counter: number;
|
117
|
105
|
|
118
|
106
|
/** the number of consecutive frames in which we have lost the tracking */
|
119
|
107
|
private _lostCounter: number;
|
120
|
108
|
|
|
109
|
+ /** camera model */
|
|
110
|
+ private _camera: CameraModel;
|
121
|
111
|
|
122
|
112
|
|
123
|
113
|
|
|
@@ -132,18 +122,14 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
132
|
122
|
this._referenceImage = null;
|
133
|
123
|
this._warpHomography = Speedy.Matrix.Eye(3);
|
134
|
124
|
this._poseHomography = Speedy.Matrix.Eye(3);
|
135
|
|
- this._initialHomography = Speedy.Matrix.Eye(3);
|
136
|
|
- this._initialKeypoints = [];
|
137
|
|
- this._counter = 0;
|
138
|
|
- this._camera = new CameraModel();
|
139
|
|
- this._predictedKeypoints = [];
|
140
|
|
- this._lastPipelineOutput = { keypoints: [] };
|
141
|
|
- this._pipelineCounter = 0;
|
|
125
|
+ this._templateKeypoints = [];
|
|
126
|
+ this._initialScreenSize = Speedy.Size(1, 1);
|
142
|
127
|
this._lastOutput = {};
|
|
128
|
+ this._lastPipelineOutput = { keypoints: [] };
|
|
129
|
+ this._skipCounter = 0;
|
|
130
|
+ this._counter = 0;
|
143
|
131
|
this._lostCounter = 0;
|
144
|
|
-
|
145
|
|
- // we need at least 4 correspondences of points to compute a homography matrix
|
146
|
|
- Utils.assert(TRACK_MIN_MATCHES >= 4);
|
|
132
|
+ this._camera = new CameraModel();
|
147
|
133
|
}
|
148
|
134
|
|
149
|
135
|
/**
|
|
@@ -152,11 +138,11 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
152
|
138
|
*/
|
153
|
139
|
onEnterState(settings: Record<string,any>)
|
154
|
140
|
{
|
155
|
|
- const homography = settings.homography as SpeedyMatrix;
|
|
141
|
+ const homography = settings.homography as SpeedyMatrix; // NDC, from reference image to video
|
156
|
142
|
const referenceImage = settings.referenceImage as Nullable<ReferenceImage>;
|
157
|
143
|
const templateKeypoints = settings.templateKeypoints as SpeedyKeypoint[];
|
158
|
|
- const keypointPortalSink = settings.keypointPortalSink as SpeedyPipelineNodeKeypointPortalSink;
|
159
|
|
- const screenSize = settings.screenSize as SpeedySize; // this.screenSize is not yet set
|
|
144
|
+ const templateKeypointPortalSink = settings.templateKeypointPortalSink as SpeedyPipelineNodeKeypointPortalSink;
|
|
145
|
+ const initialScreenSize = settings.initialScreenSize as SpeedySize; // this.screenSize is not yet set
|
160
|
146
|
const keypointPortalSource = this._pipeline.node('keypointPortalSource') as SpeedyPipelineNodeKeypointPortalSource;
|
161
|
147
|
|
162
|
148
|
// this shouldn't happen
|
|
@@ -167,20 +153,19 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
167
|
153
|
this._referenceImage = referenceImage;
|
168
|
154
|
this._warpHomography = Speedy.Matrix(homography);
|
169
|
155
|
this._poseHomography = Speedy.Matrix(homography);
|
170
|
|
- this._initialHomography = Speedy.Matrix(homography);
|
171
|
|
- this._initialKeypoints = templateKeypoints;
|
172
|
|
- this._counter = 0;
|
173
|
|
- this._predictedKeypoints = [];
|
174
|
|
- this._lastPipelineOutput = { keypoints: [] };
|
175
|
|
- this._pipelineCounter = 0;
|
|
156
|
+ this._templateKeypoints = templateKeypoints;
|
|
157
|
+ this._initialScreenSize = Speedy.Size(initialScreenSize.width, initialScreenSize.height);
|
176
|
158
|
this._lastOutput = {};
|
|
159
|
+ this._lastPipelineOutput = { keypoints: [] };
|
|
160
|
+ this._skipCounter = 0;
|
|
161
|
+ this._counter = 0;
|
177
|
162
|
this._lostCounter = 0;
|
178
|
163
|
|
179
|
164
|
// setup portals
|
180
|
|
- keypointPortalSource.source = keypointPortalSink;
|
|
165
|
+ keypointPortalSource.source = templateKeypointPortalSink;
|
181
|
166
|
|
182
|
167
|
// setup camera
|
183
|
|
- this._camera.init(screenSize);
|
|
168
|
+ this._camera.init(initialScreenSize);
|
184
|
169
|
|
185
|
170
|
// emit event
|
186
|
171
|
const ev = new ImageTrackerEvent('targetfound', referenceImage);
|
|
@@ -197,6 +182,9 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
197
|
182
|
{
|
198
|
183
|
const referenceImage = this._referenceImage as ReferenceImage;
|
199
|
184
|
|
|
185
|
+ // log
|
|
186
|
+ Utils.log(`No longer tracking image "${referenceImage.name}"!`);
|
|
187
|
+
|
200
|
188
|
// release the camera
|
201
|
189
|
this._camera.release();
|
202
|
190
|
|
|
@@ -213,7 +201,7 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
213
|
201
|
{
|
214
|
202
|
const imageRectifier = this._pipeline.node('imageRectifier') as SpeedyPipelineNodePerspectiveWarp;
|
215
|
203
|
const borderClipper = this._pipeline.node('borderClipper') as SpeedyPipelineNodeKeypointBorderClipper;
|
216
|
|
- const keypointRectifier = this._pipeline.node('keypointRectifier') as SpeedyPipelineNodeKeypointTransformer;
|
|
204
|
+ const keypointScaler = this._pipeline.node('keypointScaler') as SpeedyPipelineNodeKeypointTransformer;
|
217
|
205
|
const screenSize = this.screenSize;
|
218
|
206
|
|
219
|
207
|
/*
|
|
@@ -230,10 +218,20 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
230
|
218
|
screenSize.height * TRACK_CLIPPING_BORDER
|
231
|
219
|
);
|
232
|
220
|
|
|
221
|
+ // convert keypoints to NIS
|
|
222
|
+ keypointScaler.transform = ImageTrackerUtils.rasterToNIS(screenSize);
|
|
223
|
+
|
233
|
224
|
// rectify the image
|
234
|
|
- return this._findImageWarp(this._warpHomography, screenSize).then(warp => {
|
235
|
|
- imageRectifier.transform = warp;
|
236
|
|
- });
|
|
225
|
+ const scale = TRACK_RECTIFIED_SCALE;
|
|
226
|
+ const aspectRatio = ImageTrackerUtils.bestFitAspectRatioNDC(this._imageTracker, this._referenceImage!);
|
|
227
|
+ const shrink = ImageTrackerUtils.bestFitScaleNDC(aspectRatio, scale);
|
|
228
|
+ const undistort = this._warpHomography.inverse();
|
|
229
|
+ const toScreen = ImageTrackerUtils.NDCToRaster(screenSize);
|
|
230
|
+ const toNDC = ImageTrackerUtils.rasterToNDC(screenSize);
|
|
231
|
+
|
|
232
|
+ return imageRectifier.transform.setTo(
|
|
233
|
+ toScreen.times(shrink.times(undistort)).times(toNDC)
|
|
234
|
+ ).then(() => void 0);
|
237
|
235
|
}
|
238
|
236
|
|
239
|
237
|
/**
|
|
@@ -242,35 +240,26 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
242
|
240
|
*/
|
243
|
241
|
protected _gpuUpdate(): SpeedyPromise<SpeedyPipelineOutput>
|
244
|
242
|
{
|
245
|
|
- //return super._gpuUpdate();
|
246
|
|
-
|
247
|
243
|
// No turbo?
|
248
|
244
|
if(!USE_TURBO || Settings.powerPreference == 'low-power')
|
249
|
245
|
return super._gpuUpdate();
|
250
|
246
|
|
251
|
247
|
// When using turbo, we reduce the GPU usage by skipping every other frame
|
252
|
|
- const counter = this._pipelineCounter;
|
253
|
|
- this._pipelineCounter = (this._pipelineCounter + 1) % TURBO_SKIP;
|
254
|
|
-
|
255
|
|
- // Skip frame
|
256
|
|
- if(counter != 0) {
|
257
|
|
- if(this._lastPipelineOutput.keypoints !== undefined) {
|
258
|
|
- this._predictedKeypoints = this._predictKeypoints(
|
259
|
|
- this._lastPipelineOutput.keypoints,
|
260
|
|
- this._initialKeypoints
|
261
|
|
- );
|
262
|
|
- }
|
263
|
|
- else
|
264
|
|
- this._predictedKeypoints.length = 0;
|
|
248
|
+ if(0 == (this._skipCounter = 1 - this._skipCounter)) {
|
|
249
|
+ const templateKeypoints = this._templateKeypoints;
|
|
250
|
+ const previousKeypoints = this._lastPipelineOutput.keypoints as SpeedyMatchedKeypoint[];
|
|
251
|
+ //const currentKeypoints = this._predictKeypoints(previousKeypoints, templateKeypoints);
|
|
252
|
+ const currentKeypoints = previousKeypoints; // this actually works
|
|
253
|
+
|
|
254
|
+ this._lastPipelineOutput.keypoints = currentKeypoints;
|
265
|
255
|
|
266
|
|
- this._lastPipelineOutput.keypoints = this._predictedKeypoints;
|
267
|
256
|
return Speedy.Promise.resolve(this._lastPipelineOutput);
|
268
|
257
|
}
|
269
|
258
|
|
270
|
259
|
// Run the pipeline and store the results
|
271
|
|
- return super._gpuUpdate().then(results => {
|
272
|
|
- this._lastPipelineOutput = results;
|
273
|
|
- return results;
|
|
260
|
+ return super._gpuUpdate().then(result => {
|
|
261
|
+ this._lastPipelineOutput = result;
|
|
262
|
+ return result;
|
274
|
263
|
});
|
275
|
264
|
}
|
276
|
265
|
|
|
@@ -281,106 +270,65 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
281
|
270
|
*/
|
282
|
271
|
protected _afterUpdate(result: SpeedyPipelineOutput): SpeedyPromise<ImageTrackerStateOutput>
|
283
|
272
|
{
|
284
|
|
- const imageRectifier = this._pipeline.node('imageRectifier') as SpeedyPipelineNodePerspectiveWarp;
|
285
|
273
|
const keypoints = result.keypoints as SpeedyMatchedKeypoint[];
|
286
|
274
|
const image = result.image as SpeedyMedia | undefined;
|
287
|
|
- const referenceImage = this._referenceImage as ReferenceImage;
|
|
275
|
+ const referenceImage = this._referenceImage!;
|
|
276
|
+ const screenSize = this.screenSize;
|
|
277
|
+
|
|
278
|
+ // track the target
|
|
279
|
+ return Speedy.Promise.resolve()
|
|
280
|
+ .then(() => {
|
|
281
|
+
|
|
282
|
+ // if a change in screen size occurs, we need to recalibrate
|
|
283
|
+ // (perform a new pre-training)
|
|
284
|
+ if(!screenSize.equals(this._initialScreenSize))
|
|
285
|
+ throw new TrackingError('Detected a change in screen size');
|
288
|
286
|
|
289
|
|
- // find the best keypoint matches
|
290
|
|
- return this._preprocessMatches(keypoints, this._initialKeypoints).then(matches => {
|
|
287
|
+ // find matching pairs of keypoints
|
|
288
|
+ const allPairs = this._findMatchingPairs(this._templateKeypoints, keypoints);
|
|
289
|
+ const pairs = ImageTrackerUtils.refineMatchingPairs(allPairs);
|
|
290
|
+ if(pairs.length < TRACK_MIN_MATCHES)
|
|
291
|
+ throw new TrackingError('Not enough data points to continue the tracking');
|
291
|
292
|
|
292
|
293
|
// find motion models
|
293
|
|
- return Speedy.Promise.all<SpeedyMatrix>([
|
294
|
|
- this._findAffineMotion(matches),
|
295
|
|
- this._findPerspectiveMotion(matches)
|
|
294
|
+ const points = ImageTrackerUtils.compilePairsOfKeypointsNDC(pairs);
|
|
295
|
+ return Speedy.Promise.all<SpeedyMatrixExpr>([
|
|
296
|
+ this._findAffineMotionNDC(points),
|
|
297
|
+ this._findPerspectiveMotionNDC(points)
|
296
|
298
|
]);
|
297
|
299
|
|
298
|
|
- }).then(([affineMotion, perspectiveMotion]) => {
|
|
300
|
+ })
|
|
301
|
+ .then(([affineMotion, perspectiveMotion]) => {
|
299
|
302
|
|
300
|
303
|
const lowPower = (Settings.powerPreference == 'low-power');
|
301
|
|
- const frozen = !(!USE_TURBO || lowPower || this._counter % TURBO_SKIP == 0);
|
|
304
|
+ const delay = NUMBER_OF_PBOS * (!lowPower ? 2 : 1);
|
302
|
305
|
|
303
|
306
|
// update warp homography
|
304
|
|
- const delay = NUMBER_OF_PBOS * (!lowPower ? TURBO_SKIP : 1);
|
305
|
|
- const remainder = delay >>> 1; // we want remainder > 0, so it skips the first frame
|
306
|
|
- if(!USE_TURBO || this._counter % delay == remainder)
|
307
|
|
- this._warpHomography.setToSync(this._warpHomography.times(affineMotion));
|
|
307
|
+ if(!USE_TURBO || this._counter % delay == 1) // skip the first frame (PBOs)
|
|
308
|
+ this._warpHomography.setToSync(affineMotion.times(this._warpHomography));
|
308
|
309
|
|
309
|
310
|
// update pose homography
|
310
|
|
- if(!frozen)
|
311
|
|
- this._poseHomography.setToSync(this._warpHomography.times(perspectiveMotion));
|
|
311
|
+ this._poseHomography.setToSync(perspectiveMotion.times(this._warpHomography));
|
|
312
|
+ if(Number.isNaN(this._poseHomography.at(0,0)))
|
|
313
|
+ throw new NumericalError('Bad homography'); // normalize? 1 / h33
|
312
|
314
|
|
313
|
315
|
// update counter
|
314
|
316
|
this._counter = (this._counter + 1) % delay;
|
315
|
317
|
|
316
|
|
- // update the camera
|
317
|
|
- if(!frozen)
|
318
|
|
- return this._camera.update(this._poseHomography, this.screenSize);
|
319
|
|
- else
|
320
|
|
- return this._camera.matrix;
|
|
318
|
+ // update camera model FIXME
|
|
319
|
+ const toNDC = ImageTrackerUtils.rasterToNDC(screenSize);
|
|
320
|
+ const toScreen = ImageTrackerUtils.NDCToRaster(screenSize);
|
|
321
|
+ const homography = Speedy.Matrix(toScreen.times(this._poseHomography).times(toNDC));
|
321
|
322
|
|
322
|
|
- }).then(_ => {
|
|
323
|
+ //console.log("PIXL ", homography.toString());
|
|
324
|
+ //console.log("POSE ", this._poseHomography.toString());
|
|
325
|
+ //console.log("WARP ", this._warpHomography.toString());
|
|
326
|
+ //console.log("> AF ", Speedy.Matrix(affineMotion).toString());
|
|
327
|
+ //console.log("> PF ", Speedy.Matrix(perspectiveMotion).toString());
|
323
|
328
|
|
324
|
|
- // find the inverse of the rectification matrix
|
325
|
|
- const rectificationMatrix = imageRectifier.transform;
|
326
|
|
- const inverseRectificationMatrix = Speedy.Matrix(rectificationMatrix.inverse());
|
327
|
|
-
|
328
|
|
- // move keypoints from rectified space back to image space
|
329
|
|
- const n = keypoints.length;
|
330
|
|
- const coords: number[] = new Array(2*n);
|
331
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
332
|
|
- coords[j] = keypoints[i].position.x;
|
333
|
|
- coords[j+1] = keypoints[i].position.y;
|
334
|
|
- }
|
335
|
|
-
|
336
|
|
- return Speedy.Matrix.applyPerspectiveTransform(
|
337
|
|
- Speedy.Matrix.Zeros(2, n),
|
338
|
|
- Speedy.Matrix(2, n, coords),
|
339
|
|
- inverseRectificationMatrix
|
340
|
|
- );
|
341
|
|
-
|
342
|
|
- /*
|
343
|
|
- // test image center
|
344
|
|
- const coords2: number[] = new Array(2 * n);
|
345
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
346
|
|
- coords2[j] = this._imageTracker.screenSize.width / 2;
|
347
|
|
- coords2[j+1] = this._imageTracker.screenSize.height / 2;
|
348
|
|
- if(i % 2 == 0) {
|
349
|
|
- coords2[j] = this._imageTracker.screenSize.width / 4;
|
350
|
|
- coords2[j+1] = this._imageTracker.screenSize.height / 4;
|
351
|
|
- }
|
352
|
|
- }
|
353
|
|
-
|
354
|
|
- return Speedy.Matrix.applyPerspectiveTransform(
|
355
|
|
- Speedy.Matrix.Zeros(2, n),
|
356
|
|
- Speedy.Matrix(2, n, coords2),
|
357
|
|
- this._poseHomography
|
358
|
|
- //this._warpHomography
|
359
|
|
- );
|
360
|
|
- */
|
361
|
|
-
|
362
|
|
- }).then(mat => {
|
363
|
|
-
|
364
|
|
- /*
|
365
|
|
-
|
366
|
|
- const n = keypoints.length;
|
367
|
|
- const coords = mat.read();
|
368
|
|
-
|
369
|
|
- // ** this will interfere with the calculations when frame skipping is on **
|
370
|
|
-
|
371
|
|
- // get keypoints in image space
|
372
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
373
|
|
- keypoints[i].position.x = coords[j];
|
374
|
|
- keypoints[i].position.y = coords[j+1];
|
375
|
|
- }
|
376
|
|
-
|
377
|
|
- */
|
378
|
|
-
|
379
|
|
- // find a polyline surrounding the target
|
380
|
|
- return this._findPolyline(this._poseHomography, this.screenSize);
|
381
|
|
- //return this._findPolyline(this._warpHomography, this.screenSize);
|
382
|
|
-
|
383
|
|
- }).then(polyline => {
|
|
329
|
+ return this._camera.update(homography, screenSize);
|
|
330
|
+ })
|
|
331
|
+ .then(() => {
|
384
|
332
|
|
385
|
333
|
// we let the target object be at the origin of the world space
|
386
|
334
|
// (identity transform). We also perform a change of coordinates,
|
|
@@ -406,33 +354,34 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
406
|
354
|
viewer: viewer
|
407
|
355
|
};
|
408
|
356
|
|
409
|
|
- // build and save the output
|
410
|
|
- this._lastOutput = {
|
|
357
|
+ // tracker output
|
|
358
|
+ const trackerOutput: ImageTrackerOutput = {
|
411
|
359
|
exports: result,
|
412
|
|
- cameraMatrix: this._camera.matrix,
|
413
|
|
- homography: this._warpHomography,
|
414
|
|
- //keypoints: keypoints,
|
415
|
|
- screenSize: this.screenSize,
|
|
360
|
+ //keypointsNIS: image !== undefined ? keypoints : undefined, // debug only
|
416
|
361
|
image: image,
|
417
|
|
- polyline: polyline,
|
|
362
|
+ polylineNDC: ImageTrackerUtils.findPolylineNDC(this._poseHomography),
|
|
363
|
+ cameraMatrix: this._camera.matrix,
|
|
364
|
+ screenSize: screenSize,
|
418
|
365
|
};
|
419
|
366
|
|
|
367
|
+ // save the last output
|
|
368
|
+ this._lastOutput = trackerOutput;
|
|
369
|
+
|
420
|
370
|
// we have successfully tracked the target in this frame
|
421
|
371
|
this._lostCounter = 0;
|
422
|
372
|
|
423
|
373
|
// done!
|
424
|
374
|
return {
|
425
|
375
|
nextState: 'tracking',
|
426
|
|
- trackerOutput: this._lastOutput
|
|
376
|
+ trackerOutput: trackerOutput
|
427
|
377
|
};
|
428
|
378
|
|
429
|
|
- }).catch(err => {
|
|
379
|
+ })
|
|
380
|
+ .catch(err => {
|
430
|
381
|
|
431
|
382
|
// give some tolerance to tracking errors
|
432
|
383
|
if(err instanceof TrackingError) {
|
433
|
384
|
if(++this._lostCounter <= TRACK_LOST_TOLERANCE) {
|
434
|
|
- //console.log("ABSORB",this._lostCounter,err.toString())
|
435
|
|
- // absorb the error
|
436
|
385
|
return {
|
437
|
386
|
nextState: 'tracking',
|
438
|
387
|
trackerOutput: this._lastOutput
|
|
@@ -440,351 +389,144 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
440
|
389
|
}
|
441
|
390
|
}
|
442
|
391
|
|
443
|
|
- // lost tracking
|
|
392
|
+ // log
|
444
|
393
|
Utils.warning(`The target has been lost! ${err.toString()}`);
|
445
|
|
- this._camera.reset();
|
446
|
394
|
|
447
|
395
|
// go back to the scanning state
|
448
|
396
|
return {
|
449
|
397
|
nextState: 'scanning',
|
450
|
|
- trackerOutput: {
|
451
|
|
- image: image,
|
452
|
|
- screenSize: this.screenSize,
|
453
|
|
- },
|
|
398
|
+ trackerOutput: { }
|
454
|
399
|
};
|
455
|
400
|
|
456
|
401
|
});
|
457
|
402
|
}
|
458
|
403
|
|
459
|
404
|
/**
|
460
|
|
- * Find quality matches between two sets of keypoints
|
461
|
|
- * @param currKeypoints keypoints of the current frame
|
462
|
|
- * @param prevKeypoints keypoints of the previous frame
|
463
|
|
- * @returns quality matches
|
464
|
|
- */
|
465
|
|
- private _findQualityMatches(currKeypoints: SpeedyMatchedKeypoint[], prevKeypoints: SpeedyKeypoint[]): QualityMatches
|
466
|
|
- {
|
467
|
|
- const result: QualityMatches = [ [], [] ];
|
468
|
|
- const n = currKeypoints.length;
|
469
|
|
-
|
470
|
|
- for(let i = 0; i < n; i++) {
|
471
|
|
- const currKeypoint = currKeypoints[i];
|
472
|
|
-
|
473
|
|
- if(currKeypoint.matches[0].index >= 0 && currKeypoint.matches[1].index >= 0) {
|
474
|
|
- const d1 = currKeypoint.matches[0].distance;
|
475
|
|
- const d2 = currKeypoint.matches[1].distance;
|
476
|
|
-
|
477
|
|
- if(d1 <= TRACK_MATCH_RATIO * d2) {
|
478
|
|
- const prevKeypoint = prevKeypoints[currKeypoint.matches[0].index];
|
479
|
|
-
|
480
|
|
- result[0].push(currKeypoint);
|
481
|
|
- result[1].push(prevKeypoint);
|
482
|
|
- }
|
483
|
|
- }
|
484
|
|
- }
|
485
|
|
-
|
486
|
|
- return result;
|
487
|
|
- }
|
488
|
|
-
|
489
|
|
- /**
|
490
|
|
- * Find a better spatial distribution of the input matches
|
491
|
|
- * @param matches quality matches
|
492
|
|
- * @returns refined quality matches
|
493
|
|
- */
|
494
|
|
- private _refineQualityMatches(matches: QualityMatches): QualityMatches
|
495
|
|
- {
|
496
|
|
- const currKeypoints = matches[0];
|
497
|
|
- const prevKeypoints = matches[1];
|
498
|
|
-
|
499
|
|
- // find a better spatial distribution of the keypoints
|
500
|
|
- const indices = this._distributeKeypoints(currKeypoints, TRACK_GRID_GRANULARITY);
|
501
|
|
- const n = indices.length; // number of refined matches
|
502
|
|
-
|
503
|
|
- // assemble output
|
504
|
|
- const result: QualityMatches = [ new Array(n), new Array(n) ];
|
505
|
|
- for(let i = 0; i < n; i++) {
|
506
|
|
- result[0][i] = currKeypoints[indices[i]];
|
507
|
|
- result[1][i] = prevKeypoints[indices[i]];
|
508
|
|
- }
|
509
|
|
-
|
510
|
|
- // done!
|
511
|
|
- return result;
|
512
|
|
- }
|
513
|
|
-
|
514
|
|
- /**
|
515
|
|
- * Spatially distribute keypoints over a grid
|
516
|
|
- * @param keypoints keypoints to be distributed
|
517
|
|
- * @param gridCells number of grid elements in each axis
|
518
|
|
- * @returns a list of indices of keypoints[]
|
|
405
|
+ * Find an affine motion model in NDC between pairs of keypoints in NDC
|
|
406
|
+ * given as a 2 x 2n [ src | dest ] matrix
|
|
407
|
+ * @param points compiled pairs of keypoints in NDC
|
|
408
|
+ * @returns a promise that resolves to a 3x3 warp in NDC that maps source to destination
|
519
|
409
|
*/
|
520
|
|
- private _distributeKeypoints(keypoints: SpeedyKeypoint[], gridCells: number): number[]
|
|
410
|
+ private _findAffineMotionNDC(points: SpeedyMatrix): SpeedyPromise<SpeedyMatrixExpr>
|
521
|
411
|
{
|
522
|
|
- // get the coordinates of the keypoints
|
523
|
|
- const n = keypoints.length;
|
524
|
|
- const points: number[] = new Array(2 * n);
|
525
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
526
|
|
- points[j] = keypoints[i].x;
|
527
|
|
- points[j+1] = keypoints[i].y;
|
528
|
|
- }
|
529
|
|
-
|
530
|
|
- // normalize the coordinates to [0,1] x [0,1]
|
531
|
|
- this._normalizePoints(points);
|
532
|
|
-
|
533
|
|
- // distribute the keypoints over a grid
|
534
|
|
- const numberOfCells = gridCells * gridCells;
|
535
|
|
- const grid: number[] = (new Array(numberOfCells)).fill(-1);
|
536
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
537
|
|
- // find the grid location of the i-th point
|
538
|
|
- const xg = Math.floor(points[j] * gridCells); // 0 <= xg,yg < gridCells
|
539
|
|
- const yg = Math.floor(points[j+1] * gridCells);
|
540
|
|
-
|
541
|
|
- // store the index of the i-th point in the grid
|
542
|
|
- grid[yg * gridCells + xg] = i;
|
543
|
|
- }
|
544
|
|
-
|
545
|
|
- // retrieve points of the grid
|
546
|
|
- const indices: number[] = [];
|
547
|
|
- for(let g = 0; g < numberOfCells; g++) {
|
548
|
|
- if(grid[g] >= 0) {
|
549
|
|
- const i = grid[g];
|
550
|
|
- indices.push(i);
|
551
|
|
- }
|
552
|
|
- }
|
|
412
|
+ /*
|
553
|
413
|
|
554
|
|
- // done!
|
555
|
|
- return indices;
|
556
|
|
- }
|
|
414
|
+ We can probably get more accurate motion estimates if we
|
|
415
|
+ work in 3D rather than in 2D. We're currently estimating an
|
|
416
|
+ affine motion in 2D NDC space, which does not account for
|
|
417
|
+ perspective distortions. What if we projected the keypoints
|
|
418
|
+ into 3D NDC space, estimated the camera motion (rotation and
|
|
419
|
+ translation) that best describes the observed observed motion
|
|
420
|
+ of the keypoints, and then projected things back to 2D NDC
|
|
421
|
+ space? Need to figure this out; we'll get a homography matrix.
|
557
|
422
|
|
558
|
|
- /**
|
559
|
|
- * Normalize points to [0,1)^2
|
560
|
|
- * @param points 2 x n matrix of points in column-major format
|
561
|
|
- * @returns points
|
562
|
|
- */
|
563
|
|
- private _normalizePoints(points: number[]): number[]
|
564
|
|
- {
|
565
|
|
- Utils.assert(points.length % 2 == 0);
|
566
|
|
-
|
567
|
|
- const n = points.length / 2;
|
568
|
|
- if(n == 0)
|
569
|
|
- return points;
|
570
|
|
-
|
571
|
|
- let xmin = Number.POSITIVE_INFINITY, xmax = Number.NEGATIVE_INFINITY;
|
572
|
|
- let ymin = Number.POSITIVE_INFINITY, ymax = Number.NEGATIVE_INFINITY;
|
573
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
574
|
|
- const x = points[j], y = points[j+1];
|
575
|
|
- xmin = x < xmin ? x : xmin;
|
576
|
|
- ymin = y < ymin ? y : ymin;
|
577
|
|
- xmax = x > xmax ? x : xmax;
|
578
|
|
- ymax = y > ymax ? y : ymax;
|
579
|
|
- }
|
|
423
|
+ Note: work with a 6 DoF perspective transform instead of 8.
|
580
|
424
|
|
581
|
|
- const xlen = xmax - xmin + 1; // +1 is a correction factor, so that 0 <= x,y < 1
|
582
|
|
- const ylen = ymax - ymin + 1;
|
583
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
584
|
|
- points[j] = (points[j] - xmin) / xlen;
|
585
|
|
- points[j+1] = (points[j+1] - ymin) / ylen;
|
586
|
|
- }
|
|
425
|
+ */
|
587
|
426
|
|
588
|
|
- return points;
|
589
|
|
- }
|
|
427
|
+ return ImageTrackerUtils.findAffineWarpNDC(points, {
|
|
428
|
+ method: 'pransac',
|
|
429
|
+ reprojectionError: TRACK_RANSAC_REPROJECTIONERROR_NDC,
|
|
430
|
+ numberOfHypotheses: 512*4,
|
|
431
|
+ bundleSize: 128,
|
|
432
|
+ mask: undefined // score is not needed
|
|
433
|
+ }).then(([ warp, score ]) => {
|
590
|
434
|
|
591
|
|
- /**
|
592
|
|
- * Find a matrix with the coordinates of quality matches
|
593
|
|
- * @param matches n quality matches
|
594
|
|
- * @returns a 2 x 2n matrix split into two 2 x n blocks [ prevKeypoints | currKeypoints ]
|
595
|
|
- */
|
596
|
|
- private _findMatrixOfMatches(matches: QualityMatches): SpeedyMatrix
|
597
|
|
- {
|
598
|
|
- const n = matches[0].length;
|
599
|
|
- Utils.assert(n > 0);
|
|
435
|
+ const scale = TRACK_RECTIFIED_SCALE;
|
|
436
|
+ const aspectRatio = ImageTrackerUtils.bestFitAspectRatioNDC(this._imageTracker, this._referenceImage!);
|
|
437
|
+ const shrink = ImageTrackerUtils.bestFitScaleNDC(aspectRatio, scale);
|
|
438
|
+ const grow = ImageTrackerUtils.inverseBestFitScaleNDC(aspectRatio, scale);
|
|
439
|
+ const scaledWarp = grow.times(warp).times(shrink);
|
600
|
440
|
|
601
|
|
- // sets of keypoints
|
602
|
|
- const currKeypoints = matches[0];
|
603
|
|
- const prevKeypoints = matches[1];
|
|
441
|
+ const distort = this._warpHomography;
|
|
442
|
+ const undistort = distort.inverse();
|
|
443
|
+ const correctedWarp = distort.times(scaledWarp).times(undistort);
|
604
|
444
|
|
605
|
|
- // get the coordinates of the keypoints of the set of refined matches
|
606
|
|
- const src: number[] = new Array(2*n);
|
607
|
|
- const dst: number[] = new Array(2*n);
|
|
445
|
+ return correctedWarp;
|
608
|
446
|
|
609
|
|
- for(let i = 0, j = 0; i < n; i++, j += 2) {
|
610
|
|
- src[j] = prevKeypoints[i].x;
|
611
|
|
- src[j+1] = prevKeypoints[i].y;
|
|
447
|
+ }).catch(err => {
|
612
|
448
|
|
613
|
|
- dst[j] = currKeypoints[i].x;
|
614
|
|
- dst[j+1] = currKeypoints[i].y;
|
615
|
|
- }
|
|
449
|
+ throw new TrackingError(`Can't find an affine motion`, err);
|
616
|
450
|
|
617
|
|
- // assemble the matrix
|
618
|
|
- return Speedy.Matrix(2, 2*n, src.concat(dst));
|
|
451
|
+ });
|
619
|
452
|
}
|
620
|
453
|
|
621
|
454
|
/**
|
622
|
|
- * Preprocess keypoint matches
|
623
|
|
- * @param currKeypoints keypoints of the current frame
|
624
|
|
- * @param prevKeypoints keypoints of the previous frame
|
625
|
|
- * @returns a promise that is rejected if there are not enough "good" matches, or that is resolved to a
|
626
|
|
- * 2 x 2n matrix split into two 2 x n blocks [ source x,y coordinates | dest x,y coordinates ]
|
|
455
|
+ * Find a perspective motion model in NDC between pairs of keypoints in NDC
|
|
456
|
+ * given as a 2 x 2n [ src | dest ] matrix
|
|
457
|
+ * @param points compiled pairs of keypoints in NDC
|
|
458
|
+ * @returns a promise that resolves to a 3x3 warp in NDC that maps source to destination
|
627
|
459
|
*/
|
628
|
|
- private _preprocessMatches(currKeypoints: SpeedyMatchedKeypoint[], prevKeypoints: SpeedyKeypoint[]): SpeedyPromise<SpeedyMatrix>
|
|
460
|
+ private _findPerspectiveMotionNDC(points: SpeedyMatrix): SpeedyPromise<SpeedyMatrixExpr>
|
629
|
461
|
{
|
630
|
|
- // find and refine quality matches
|
631
|
|
- const qualityMatches = this._findQualityMatches(currKeypoints, prevKeypoints);
|
632
|
|
- const refinedMatches = this._refineQualityMatches(qualityMatches);
|
633
|
|
-
|
634
|
|
- // not enough matches?
|
635
|
|
- const n = refinedMatches[0].length;
|
636
|
|
- if(n < TRACK_MIN_MATCHES)
|
637
|
|
- return Speedy.Promise.reject(new TrackingError('Not enough data to compute a motion model'));
|
638
|
|
-
|
639
|
|
- // find matrix of matches
|
640
|
|
- const matrixOfMatches = this._findMatrixOfMatches(refinedMatches);
|
|
462
|
+ return ImageTrackerUtils.findPerspectiveWarpNDC(points, {
|
|
463
|
+ method: 'pransac',
|
|
464
|
+ reprojectionError: TRACK_RANSAC_REPROJECTIONERROR_NDC,
|
|
465
|
+ numberOfHypotheses: 512*2,
|
|
466
|
+ bundleSize: 128,//128*4,
|
|
467
|
+ mask: undefined // score is not needed
|
|
468
|
+ }).then(([ warp, score ]) => {
|
641
|
469
|
|
642
|
|
- // warp matrix of matches
|
643
|
|
- const result = Speedy.Matrix.Zeros(2, 2*n);
|
644
|
|
- return this._findKeypointWarp().then(transform =>
|
|
470
|
+ const scale = TRACK_RECTIFIED_SCALE;
|
|
471
|
+ const aspectRatio = ImageTrackerUtils.bestFitAspectRatioNDC(this._imageTracker, this._referenceImage!);
|
|
472
|
+ const shrink = ImageTrackerUtils.bestFitScaleNDC(aspectRatio, scale);
|
|
473
|
+ const grow = ImageTrackerUtils.inverseBestFitScaleNDC(aspectRatio, scale);
|
|
474
|
+ const scaledWarp = grow.times(warp).times(shrink);
|
645
|
475
|
|
646
|
|
- Speedy.Matrix.applyAffineTransform(
|
647
|
|
- result,
|
648
|
|
- matrixOfMatches,
|
649
|
|
- transform.block(0,1,0,2)
|
650
|
|
- )
|
|
476
|
+ const distort = this._poseHomography;
|
|
477
|
+ const undistort = distort.inverse();
|
|
478
|
+ const correctedWarp = distort.times(scaledWarp).times(undistort);
|
651
|
479
|
|
652
|
|
- );
|
653
|
|
- }
|
|
480
|
+ return correctedWarp;
|
654
|
481
|
|
655
|
|
- /**
|
656
|
|
- * Find an affine motion model of the target image
|
657
|
|
- * @param preprocessedMatches 2 x 2n matrix split into two 2 x n blocks [ src | dest ]
|
658
|
|
- * @returns a promise that resolves to a 3x3 affine motion model (last row is [ 0 0 1 ])
|
659
|
|
- */
|
660
|
|
- private _findAffineMotion(preprocessedMatches: SpeedyMatrix): SpeedyPromise<SpeedyMatrix>
|
661
|
|
- {
|
662
|
|
- const model = Speedy.Matrix.Eye(3);
|
663
|
|
- const n = preprocessedMatches.columns / 2; // number of preprocessed matches
|
664
|
|
-
|
665
|
|
- // find motion model
|
666
|
|
- return Speedy.Matrix.findAffineTransform(
|
667
|
|
- model.block(0,1,0,2),
|
668
|
|
- preprocessedMatches.block(0,1,0,n-1),
|
669
|
|
- preprocessedMatches.block(0,1,n,2*n-1), {
|
670
|
|
- method: 'pransac',
|
671
|
|
- reprojectionError: TRACK_RANSAC_REPROJECTIONERROR,
|
672
|
|
- numberOfHypotheses: 512,
|
673
|
|
- bundleSize: 128,
|
674
|
|
- }).then(_ => {
|
675
|
|
-
|
676
|
|
- // validate the model
|
677
|
|
- const a00 = model.at(0,0);
|
678
|
|
- if(Number.isNaN(a00))
|
679
|
|
- throw new TrackingError(`Can't compute affine motion model: bad keypoints`);
|
|
482
|
+ }).catch(err => {
|
680
|
483
|
|
681
|
|
- // done!
|
682
|
|
- return model;
|
|
484
|
+ throw new TrackingError(`Can't find a perspective motion`, err);
|
683
|
485
|
|
684
|
486
|
});
|
685
|
487
|
}
|
686
|
488
|
|
687
|
489
|
/**
|
688
|
|
- * Find a perspective motion model of the target image
|
689
|
|
- * @param preprocessedMatches 2 x 2n matrix split into two 2 x n blocks [ src | dest ]
|
690
|
|
- * @returns a promise that resolves to a 3x3 perspective motion model
|
|
490
|
+ * Find matching pairs of two sets of keypoints matched via brute force
|
|
491
|
+ * @param srcKeypoints source (database)
|
|
492
|
+ * @param destKeypoints destination
|
|
493
|
+ * @returns an array of matching pairs [src, dest]
|
691
|
494
|
*/
|
692
|
|
- private _findPerspectiveMotion(preprocessedMatches: SpeedyMatrix): SpeedyPromise<SpeedyMatrix>
|
|
495
|
+ private _findMatchingPairs(srcKeypoints: SpeedyKeypoint[], destKeypoints: SpeedyMatchedKeypoint[]): ImageTrackerKeypointPair[]
|
693
|
496
|
{
|
694
|
|
- /*
|
695
|
|
-
|
696
|
|
- We can probably get more accurate motion estimates if we
|
697
|
|
- work in 3D rather than in 2D. We're currently estimating
|
698
|
|
- an affine transform in image space. What if we projected
|
699
|
|
- the keypoints into world space, estimated the camera motion
|
700
|
|
- (rotation and translation) that best describes the observed
|
701
|
|
- observed motion of the keypoints, and then projected things
|
702
|
|
- back to image space? Need to figure this out; we'll get a
|
703
|
|
- homography matrix.
|
704
|
|
-
|
705
|
|
- Note: keypoints are in rectified image space.
|
706
|
|
-
|
707
|
|
- Note: work with a 6 DoF perspective transform instead of 8.
|
708
|
|
-
|
709
|
|
- */
|
710
|
|
-
|
711
|
|
- const model = Speedy.Matrix.Zeros(3);
|
712
|
|
- const n = preprocessedMatches.columns / 2; // number of preprocessed matches
|
713
|
|
-
|
714
|
|
- // find motion model
|
715
|
|
- return Speedy.Matrix.findHomography(
|
716
|
|
- model,
|
717
|
|
- preprocessedMatches.block(0,1,0,n-1),
|
718
|
|
- preprocessedMatches.block(0,1,n,2*n-1), {
|
719
|
|
- method: 'pransac',
|
720
|
|
- reprojectionError: TRACK_RANSAC_REPROJECTIONERROR,
|
721
|
|
- numberOfHypotheses: 512*2,
|
722
|
|
- bundleSize: 128*4, //*4
|
723
|
|
- }).then(_ => {
|
724
|
|
-
|
725
|
|
- // validate the model
|
726
|
|
- const a00 = model.at(0,0);
|
727
|
|
- if(Number.isNaN(a00))
|
728
|
|
- throw new TrackingError(`Can't compute perspective motion model: bad keypoints`);
|
729
|
|
-
|
730
|
|
- // done!
|
731
|
|
- return model;
|
|
497
|
+ const pairs: ImageTrackerKeypointPair[] = [];
|
732
|
498
|
|
733
|
|
- });
|
734
|
|
- }
|
|
499
|
+ for(let i = 0; i < destKeypoints.length; i++) {
|
|
500
|
+ const destKeypoint = destKeypoints[i];
|
735
|
501
|
|
736
|
|
- /**
|
737
|
|
- * Find a rectification matrix to be applied to the target image
|
738
|
|
- * @param homography maps a reference image to the AR screen
|
739
|
|
- * @param media target
|
740
|
|
- * @param screenSize AR screen
|
741
|
|
- * @returns promise that resolves to a rectification matrix
|
742
|
|
- */
|
743
|
|
- private _findImageWarp(homography: SpeedyMatrix, screenSize: SpeedySize): SpeedyPromise<SpeedyMatrix>
|
744
|
|
- {
|
745
|
|
- const referenceImage = this._referenceImage as ReferenceImage;
|
746
|
|
- const media = this._imageTracker.database._findMedia(referenceImage.name);
|
747
|
|
- const mat = Speedy.Matrix.Zeros(3);
|
|
502
|
+ if(destKeypoint.matches[0].index >= 0 && destKeypoint.matches[1].index >= 0) {
|
|
503
|
+ const d1 = destKeypoint.matches[0].distance;
|
|
504
|
+ const d2 = destKeypoint.matches[1].distance;
|
748
|
505
|
|
749
|
|
- return this._findRectificationMatrixOfFullscreenImage(media, screenSize).then(warp =>
|
750
|
|
- mat.setTo(warp.times(homography.inverse()))
|
751
|
|
- );
|
752
|
|
- }
|
|
506
|
+ // the best match should be "much better" than the second best match,
|
|
507
|
+ // which means that they are "distinct enough"
|
|
508
|
+ if(d1 <= TRACK_MATCH_RATIO * d2) {
|
|
509
|
+ const srcKeypoint = srcKeypoints[destKeypoint.matches[0].index];
|
|
510
|
+ pairs.push([srcKeypoint, destKeypoint]);
|
|
511
|
+ }
|
|
512
|
+ }
|
|
513
|
+ }
|
753
|
514
|
|
754
|
|
- /**
|
755
|
|
- * Find a warp to be applied to the keypoints
|
756
|
|
- * @returns affine transform
|
757
|
|
- */
|
758
|
|
- private _findKeypointWarp(): SpeedyPromise<SpeedyMatrix>
|
759
|
|
- {
|
760
|
|
- const referenceImage = this._referenceImage as ReferenceImage;
|
761
|
|
- const media = this._imageTracker.database._findMedia(referenceImage.name);
|
762
|
|
- const screenSize = this.screenSize;
|
763
|
|
- const sw = screenSize.width, sh = screenSize.height;
|
764
|
|
- const mat = Speedy.Matrix.Eye(3, 3);
|
765
|
|
-
|
766
|
|
- // no rotation is needed
|
767
|
|
- if(!this._mustRotateWarpedImage(media, screenSize))
|
768
|
|
- return Speedy.Promise.resolve(mat);
|
769
|
|
-
|
770
|
|
- // rotate by 90 degrees clockwise and scale
|
771
|
|
- return Speedy.Matrix.affine(
|
772
|
|
- mat.block(0,1,0,2),
|
773
|
|
- Speedy.Matrix(2, 3, [ 0,sh , 0,0 , sw,0 ]),
|
774
|
|
- Speedy.Matrix(2, 3, [ 0,0 , sw,0 , sw,sh ])
|
775
|
|
- ).then(_ => mat);
|
|
515
|
+ return pairs;
|
776
|
516
|
}
|
777
|
517
|
|
778
|
518
|
/**
|
779
|
519
|
* Predict the keypoints without actually looking at the image
|
780
|
520
|
* @param curr keypoints at time t (will modify the contents)
|
781
|
|
- * @param initial keypoints at time t-1 (not just t = 0)
|
|
521
|
+ * @param prev keypoints at time t-1 (not just t = 0)
|
782
|
522
|
* @returns keypoints at time t+1
|
783
|
523
|
*/
|
784
|
|
- private _predictKeypoints(curr: SpeedyMatchedKeypoint[], initial: SpeedyKeypoint[]): SpeedyMatchedKeypoint[]
|
|
524
|
+ /*
|
|
525
|
+ private _predictKeypoints(curr: SpeedyMatchedKeypoint[], prev: SpeedyKeypoint[]): SpeedyMatchedKeypoint[]
|
785
|
526
|
{
|
786
|
527
|
// the target image is likely to be moving roughly in
|
787
|
528
|
// the same manner as it was in the previous frame
|
|
529
|
+ const alpha = 0.8; //0.2;
|
788
|
530
|
const next: SpeedyMatchedKeypoint[] = [];
|
789
|
531
|
const n = curr.length;
|
790
|
532
|
|
|
@@ -793,25 +535,23 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
793
|
535
|
|
794
|
536
|
if(cur.matches[0].index < 0 || cur.matches[1].index < 0)
|
795
|
537
|
continue;
|
796
|
|
- /*
|
797
|
|
- else if(cur.matches[0].distance > TRACK_MATCH_RATIO * cur.matches[1].distance)
|
798
|
|
- continue;
|
799
|
|
- */
|
|
538
|
+ //else if(cur.matches[0].distance > TRACK_MATCH_RATIO * cur.matches[1].distance)
|
|
539
|
+ // continue;
|
800
|
540
|
|
801
|
|
- const ini = initial[cur.matches[0].index];
|
802
|
|
- const dx = cur.position.x - ini.position.x;
|
803
|
|
- const dy = cur.position.y - ini.position.y;
|
|
541
|
+ const prv = prev[cur.matches[0].index];
|
|
542
|
+ const dx = cur.position.x - prv.position.x;
|
|
543
|
+ const dy = cur.position.y - prv.position.y;
|
804
|
544
|
|
805
|
545
|
// a better mathematical model is needed
|
806
|
|
- const alpha = 0.8; //0.2;
|
807
|
|
- cur.position.x = ini.position.x + alpha * dx;
|
808
|
|
- cur.position.y = ini.position.y + alpha * dy;
|
|
546
|
+ cur.position.x = prv.position.x + alpha * dx;
|
|
547
|
+ cur.position.y = prv.position.y + alpha * dy;
|
809
|
548
|
next.push(cur);
|
810
|
549
|
}
|
811
|
550
|
|
812
|
551
|
// done!
|
813
|
552
|
return next;
|
814
|
553
|
}
|
|
554
|
+ */
|
815
|
555
|
|
816
|
556
|
/**
|
817
|
557
|
* Create & setup the pipeline
|
|
@@ -835,10 +575,10 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
835
|
575
|
const denoiser = Speedy.Filter.GaussianBlur();
|
836
|
576
|
const borderClipper = Speedy.Keypoint.BorderClipper('borderClipper');
|
837
|
577
|
const clipper = Speedy.Keypoint.Clipper();
|
838
|
|
- const keypointRectifier = Speedy.Keypoint.Transformer('keypointRectifier');
|
|
578
|
+ const keypointScaler = Speedy.Keypoint.Transformer('keypointScaler');
|
839
|
579
|
const keypointPortalSource = Speedy.Keypoint.Portal.Source('keypointPortalSource');
|
840
|
580
|
const keypointSink = Speedy.Keypoint.SinkOfMatchedKeypoints('keypoints');
|
841
|
|
- const imageSink = Speedy.Image.Sink('image');
|
|
581
|
+ //const imageSink = Speedy.Image.Sink('image');
|
842
|
582
|
|
843
|
583
|
source.media = null;
|
844
|
584
|
screen.size = Speedy.Size(0,0);
|
|
@@ -858,7 +598,7 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
858
|
598
|
clipper.size = TRACK_MAX_KEYPOINTS;
|
859
|
599
|
borderClipper.imageSize = screen.size;
|
860
|
600
|
borderClipper.borderSize = Speedy.Vector2(0,0);
|
861
|
|
- keypointRectifier.transform = Speedy.Matrix.Eye(3);
|
|
601
|
+ keypointScaler.transform = Speedy.Matrix.Eye(3);
|
862
|
602
|
matcher.k = 2;
|
863
|
603
|
keypointPortalSource.source = null;
|
864
|
604
|
keypointSink.turbo = USE_TURBO;
|
|
@@ -893,9 +633,8 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
893
|
633
|
descriptor.output().connectTo(matcher.input('keypoints'));
|
894
|
634
|
|
895
|
635
|
// prepare output
|
896
|
|
- descriptor.output().connectTo(keypointRectifier.input());
|
897
|
|
- //preMatcher.output().connectTo(keypointRectifier.input());
|
898
|
|
- keypointRectifier.output().connectTo(keypointSink.input());
|
|
636
|
+ descriptor.output().connectTo(keypointScaler.input());
|
|
637
|
+ keypointScaler.output().connectTo(keypointSink.input());
|
899
|
638
|
matcher.output().connectTo(keypointSink.input('matches'));
|
900
|
639
|
//imageRectifier.output().connectTo(imageSink.input());
|
901
|
640
|
|
|
@@ -905,9 +644,10 @@ export class ImageTrackerTrackingState extends ImageTrackerState
|
905
|
644
|
imageRectifier, nightvision, nightvisionMux, blur,
|
906
|
645
|
detector, subpixel, borderClipper, clipper, denoiser,
|
907
|
646
|
descriptor, matcher,
|
908
|
|
- keypointPortalSource, keypointRectifier, keypointSink,
|
|
647
|
+ keypointPortalSource, keypointScaler, keypointSink,
|
909
|
648
|
//imageSink
|
910
|
649
|
);
|
|
650
|
+
|
911
|
651
|
return pipeline;
|
912
|
652
|
}
|
913
|
653
|
}
|