|
@@ -30,14 +30,8 @@ import { Nullable, Utils } from '../utils/utils';
|
30
|
30
|
import { Settings } from '../core/settings';
|
31
|
31
|
import { IllegalOperationError, IllegalArgumentError } from '../utils/errors';
|
32
|
32
|
|
33
|
|
-/** Number of samples we'll be keeping to help calibrate the camera */
|
34
|
|
-const INTRISICS_SAMPLES = 401; //201; //31; // odd number
|
35
|
|
-
|
36
|
|
-/** Whether or not to auto-calibrate the camera */
|
37
|
|
-const FOVY_AUTODETECT = false; //true;
|
38
|
|
-
|
39
|
|
-/** A guess of the vertical field-of-view of a generic camera, in degrees */
|
40
|
|
-const FOVY_GUESS = 45; //50; // will be part of the viewing frustum
|
|
33
|
+/** A guess of the horizontal field-of-view of the camera, in degrees */
|
|
34
|
+const HFOV_GUESS = 60; // https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Cameras/Cameras.html
|
41
|
35
|
|
42
|
36
|
/** Number of iterations used to refine the estimated pose */
|
43
|
37
|
const POSE_ITERATIONS = 30;
|
|
@@ -117,12 +111,6 @@ export class CameraModel
|
117
|
111
|
/** extrinsics matrix, in column-major format */
|
118
|
112
|
private _extrinsics: number[];
|
119
|
113
|
|
120
|
|
- /** estimates of the focal length */
|
121
|
|
- private _f: number[];
|
122
|
|
-
|
123
|
|
- /** index/pointer of _f[] */
|
124
|
|
- private _fp: number;
|
125
|
|
-
|
126
|
114
|
/** filter: samples of partial rotation matrix [ r1 | r2 ] */
|
127
|
115
|
private _partialRotationBuffer: number[][];
|
128
|
116
|
|
|
@@ -140,8 +128,6 @@ export class CameraModel
|
140
|
128
|
this._matrix = Speedy.Matrix.Eye(3, 4);
|
141
|
129
|
this._intrinsics = [1,0,0,0,1,0,0,0,1]; // identity matrix
|
142
|
130
|
this._extrinsics = [1,0,0,0,1,0,0,0,1,0,0,0]; // no rotation & no translation [ R | t ] = [ I | 0 ]
|
143
|
|
- this._f = (new Array(INTRISICS_SAMPLES)).fill(this._intrinsics[FY]);
|
144
|
|
- this._fp = 0;
|
145
|
131
|
this._partialRotationBuffer = [];
|
146
|
132
|
this._translationBuffer = [];
|
147
|
133
|
}
|
|
@@ -218,12 +204,6 @@ export class CameraModel
|
218
|
204
|
return Speedy.Promise.resolve(this._matrix);
|
219
|
205
|
}
|
220
|
206
|
|
221
|
|
- // estimate the focal length (auto-calibration)
|
222
|
|
- const f = this._estimateFocal(homography);
|
223
|
|
- if(f > 0)
|
224
|
|
- this._storeFocal(f);
|
225
|
|
- //console.log(this.fovy * RAD2DEG);
|
226
|
|
-
|
227
|
207
|
// estimate the pose
|
228
|
208
|
const pose = this._estimatePose(homography);
|
229
|
209
|
this._storePose(pose);
|
|
@@ -379,98 +359,41 @@ export class CameraModel
|
379
|
359
|
*/
|
380
|
360
|
private _resetIntrinsics(): void
|
381
|
361
|
{
|
|
362
|
+ const cameraWidth = Math.max(this._screenSize.width, this._screenSize.height); // portrait?
|
382
|
363
|
const u0 = this._screenSize.width / 2;
|
383
|
364
|
const v0 = this._screenSize.height / 2;
|
384
|
|
- const f = v0 / Math.tan(DEG2RAD * FOVY_GUESS / 2);
|
|
365
|
+ const fx = (cameraWidth / 2) / Math.tan(DEG2RAD * HFOV_GUESS / 2);
|
|
366
|
+ const fy = fx;
|
385
|
367
|
|
386
|
|
- this._intrinsics[FX] = f;
|
387
|
|
- this._intrinsics[FY] = f;
|
|
368
|
+ this._intrinsics[FX] = fx;
|
|
369
|
+ this._intrinsics[FY] = fy;
|
388
|
370
|
this._intrinsics[U0] = u0;
|
389
|
371
|
this._intrinsics[V0] = v0;
|
390
|
|
-
|
391
|
|
- this._f.fill(this._intrinsics[FY]);
|
392
|
|
- this._fp = 0;
|
393
|
|
- }
|
394
|
|
-
|
395
|
|
- /**
|
396
|
|
- * Estimate the focal length
|
397
|
|
- * @param homography valid homography
|
398
|
|
- * @returns estimated focal length, or 0 on error
|
399
|
|
- */
|
400
|
|
- private _estimateFocal(homography: SpeedyMatrix): number
|
401
|
|
- {
|
402
|
|
- // auto-detect the focal length?
|
403
|
|
- if(!FOVY_AUTODETECT)
|
404
|
|
- return 0;
|
405
|
|
-
|
406
|
|
- // read the entries of the homography
|
407
|
|
- const h = homography.read();
|
408
|
|
- const h11 = h[0], h12 = h[3];//, h13 = h[6];
|
409
|
|
- const h21 = h[1], h22 = h[4];//, h23 = h[7];
|
410
|
|
- const h31 = h[2], h32 = h[5];//, h33 = h[8];
|
411
|
|
-
|
412
|
|
- // read the principal point
|
413
|
|
- const u0 = this._intrinsics[U0];
|
414
|
|
- const v0 = this._intrinsics[V0];
|
415
|
|
-
|
416
|
|
- // estimate the focal length based on the orthogonality
|
417
|
|
- // constraint r1'r2 = 0 of a rotation matrix
|
418
|
|
- const f2 = -((h11/h31 - u0) * (h12/h32 - u0) + (h21/h31 - v0) * (h22/h32 - v0));
|
419
|
|
-
|
420
|
|
- // can't estimate it?
|
421
|
|
- if(f2 < 0)
|
422
|
|
- return this._intrinsics[FY];
|
423
|
|
- //return 0;
|
424
|
|
-
|
425
|
|
- // done!
|
426
|
|
- return Math.sqrt(f2);
|
427
|
|
- }
|
428
|
|
-
|
429
|
|
- /**
|
430
|
|
- * Store an estimated focal length
|
431
|
|
- * @param f estimated focal length
|
432
|
|
- */
|
433
|
|
- private _storeFocal(f: number): void
|
434
|
|
- {
|
435
|
|
- // store the focal length
|
436
|
|
- this._f[this._fp] = f;
|
437
|
|
- this._fp = (this._fp + 1) % INTRISICS_SAMPLES;
|
438
|
|
-
|
439
|
|
- // take the median of the estimated focal lengths
|
440
|
|
- const sorted = this._f.concat([]).sort((a, b) => a - b);
|
441
|
|
- const median = sorted[sorted.length >>> 1];
|
442
|
|
-
|
443
|
|
- // update the intrinsics matrix
|
444
|
|
- this._intrinsics[FX] = this._intrinsics[FY] = median;
|
445
|
|
-
|
446
|
|
- /*
|
447
|
|
- // test
|
448
|
|
- const u0 = this._intrinsics[U0];
|
449
|
|
- const v0 = this._intrinsics[V0];
|
450
|
|
- const fovx = 2 * Math.atan(u0 / median) * RAD2DEG;
|
451
|
|
- const fovy = 2 * Math.atan(v0 / median) * RAD2DEG;
|
452
|
|
- console.log('---------------');
|
453
|
|
- console.log("fov:",fovx,fovy);
|
454
|
|
- console.log("f:",median);
|
455
|
|
- */
|
456
|
372
|
}
|
457
|
373
|
|
458
|
374
|
/**
|
459
|
375
|
* Compute a normalized homography H' = K^(-1) * H for an
|
460
|
376
|
* ideal pinhole with f = 1 and principal point = (0,0)
|
461
|
377
|
* @param homography homography H to be normalized
|
462
|
|
- * @param f focal length
|
463
|
378
|
* @returns normalized homography H'
|
464
|
379
|
*/
|
465
|
|
- private _normalizeHomography(homography: SpeedyMatrix, f: number = this._intrinsics[FY]): SpeedyMatrix
|
|
380
|
+ private _normalizeHomography(homography: SpeedyMatrix): SpeedyMatrix
|
466
|
381
|
{
|
467
|
382
|
const h = homography.read();
|
468
|
383
|
const u0 = this._intrinsics[U0];
|
469
|
384
|
const v0 = this._intrinsics[V0];
|
|
385
|
+ const fx = this._intrinsics[FX];
|
|
386
|
+ const fy = this._intrinsics[FY];
|
470
|
387
|
|
471
|
|
- const h11 = h[0] - u0 * h[2], h12 = h[3] - u0 * h[5], h13 = h[6] - u0 * h[8];
|
472
|
|
- const h21 = h[1] - v0 * h[2], h22 = h[4] - v0 * h[5], h23 = h[7] - v0 * h[8];
|
473
|
|
- const h31 = h[2] * f, h32 = h[5] * f, h33 = h[8] * f;
|
|
388
|
+ const h11 = (h[0] - u0 * h[2]) / fx, h12 = (h[3] - u0 * h[5]) / fx, h13 = (h[6] - u0 * h[8]) / fx;
|
|
389
|
+ const h21 = (h[1] - v0 * h[2]) / fy, h22 = (h[4] - v0 * h[5]) / fy, h23 = (h[7] - v0 * h[8]) / fy;
|
|
390
|
+ const h31 = h[2], h32 = h[5], h33 = h[8];
|
|
391
|
+
|
|
392
|
+ /*console.log([
|
|
393
|
+ h11, h21, h31,
|
|
394
|
+ h12, h22, h32,
|
|
395
|
+ h13, h23, h33,
|
|
396
|
+ ]);*/
|
474
|
397
|
|
475
|
398
|
return Speedy.Matrix(3, 3, [
|
476
|
399
|
h11, h21, h31,
|
|
@@ -495,12 +418,14 @@ export class CameraModel
|
495
|
418
|
const sign = h33 >= 0 ? 1 : -1;
|
496
|
419
|
|
497
|
420
|
// compute the scale factor
|
498
|
|
- const h1norm = Math.sqrt(h11 * h11 + h21 * h21 + h31 * h31);
|
499
|
|
- const h2norm = Math.sqrt(h12 * h12 + h22 * h22 + h32 * h32);
|
500
|
|
- //const scale = sign * 2 / (h1norm + h2norm);
|
501
|
|
- //const scale = sign / h1norm;
|
502
|
|
- //const scale = sign / h2norm;
|
503
|
|
- const scale = sign / Math.max(h1norm, h2norm); // this seems to work. why?
|
|
421
|
+ const h1norm2 = h11 * h11 + h21 * h21 + h31 * h31;
|
|
422
|
+ const h2norm2 = h12 * h12 + h22 * h22 + h32 * h32;
|
|
423
|
+ const h1norm = Math.sqrt(h1norm2);
|
|
424
|
+ const h2norm = Math.sqrt(h2norm2);
|
|
425
|
+ //const hnorm = (h1norm + h2norm) / 2;
|
|
426
|
+ //const hnorm = Math.sqrt(h1norm * h2norm);
|
|
427
|
+ const hnorm = Math.max(h1norm, h2norm); // this seems to work. why?
|
|
428
|
+ const scale = sign / hnorm;
|
504
|
429
|
|
505
|
430
|
// invalid homography?
|
506
|
431
|
if(Number.isNaN(scale))
|
|
@@ -515,23 +440,24 @@ export class CameraModel
|
515
|
440
|
//console.log("h1,h2",h1norm,h2norm);
|
516
|
441
|
//console.log(normalizedHomography.toString());
|
517
|
442
|
|
518
|
|
- // recover the translation and the rotation
|
519
|
|
- const t1 = scale * h13;
|
520
|
|
- const t2 = scale * h23;
|
521
|
|
- const t3 = scale * h33;
|
522
|
|
-
|
|
443
|
+ // recover the pose
|
523
|
444
|
const r11 = scale * h11;
|
524
|
445
|
const r21 = scale * h21;
|
525
|
446
|
const r31 = scale * h31;
|
526
|
|
-
|
527
|
447
|
const r12 = scale * h12;
|
528
|
448
|
const r22 = scale * h22;
|
529
|
449
|
const r32 = scale * h32;
|
|
450
|
+ const r_ = [r11, r21, r31, r12, r22, r32];
|
|
451
|
+
|
|
452
|
+ const t1 = scale * h13;
|
|
453
|
+ const t2 = scale * h23;
|
|
454
|
+ const t3 = scale * h33;
|
|
455
|
+ const t_ = [t1, t2, t3];
|
530
|
456
|
|
531
|
457
|
// refine the pose
|
532
|
|
- const r = this._refineRotation(r11, r21, r31, r12, r22, r32);
|
533
|
|
- const t = this._refineTranslation(normalizedHomography, r, [t1, t2, t3]);
|
534
|
|
- //const t = [t1, t2, t3]; // faster, but less accurate
|
|
458
|
+ const r = this._refineRotation(r_);
|
|
459
|
+ const t = this._refineTranslation(normalizedHomography, r, t_);
|
|
460
|
+ //const t = t_; // faster, but less accurate
|
535
|
461
|
|
536
|
462
|
// done!
|
537
|
463
|
return Speedy.Matrix(3, 3, r.concat(t)); // this is possibly NaN... why? homography...
|
|
@@ -539,16 +465,13 @@ export class CameraModel
|
539
|
465
|
|
540
|
466
|
/**
|
541
|
467
|
* Make two non-zero and non-parallel input vectors, r1 and r2, orthonormal
|
542
|
|
- * @param r11 x of r1
|
543
|
|
- * @param r21 y of r1
|
544
|
|
- * @param r31 z of r1
|
545
|
|
- * @param r12 x of r2
|
546
|
|
- * @param r22 y of r2
|
547
|
|
- * @param r32 z of r2
|
|
468
|
+ * @param rot rotation vectors [ r1 | r2 ] in column-major format
|
548
|
469
|
* @returns a 3x2 matrix R such that R'R = I (column-major format)
|
549
|
470
|
*/
|
550
|
|
- private _refineRotation(r11: number, r21: number, r31: number, r12: number, r22: number, r32: number): number[]
|
|
471
|
+ private _refineRotation(rot: number[]): number[]
|
551
|
472
|
{
|
|
473
|
+ const [r11, r21, r31, r12, r22, r32] = rot;
|
|
474
|
+
|
552
|
475
|
/*
|
553
|
476
|
|
554
|
477
|
A little technique I figured out to correct the rotation vectors
|
|
@@ -853,7 +776,7 @@ export class CameraModel
|
853
|
776
|
for(let j = 0; j < 6; j++)
|
854
|
777
|
avg[j] += r[j] / n;
|
855
|
778
|
}
|
856
|
|
- const r = this._refineRotation(avg[0], avg[1], avg[2], avg[3], avg[4], avg[5]);
|
|
779
|
+ const r = this._refineRotation(avg);
|
857
|
780
|
|
858
|
781
|
// average translations
|
859
|
782
|
const m = this._translationBuffer.length;
|
|
@@ -906,12 +829,11 @@ export class CameraModel
|
906
|
829
|
/**
|
907
|
830
|
* Estimate the pose [ R | t ] given a homography in AR screen space
|
908
|
831
|
* @param homography must be valid
|
909
|
|
- * @param f focal length
|
910
|
832
|
* @returns 3x4 matrix
|
911
|
833
|
*/
|
912
|
|
- private _estimatePose(homography: SpeedyMatrix, f: number = this._intrinsics[FY]): SpeedyMatrix
|
|
834
|
+ private _estimatePose(homography: SpeedyMatrix): SpeedyMatrix
|
913
|
835
|
{
|
914
|
|
- const normalizedHomography = this._normalizeHomography(homography, f);
|
|
836
|
+ const normalizedHomography = this._normalizeHomography(homography);
|
915
|
837
|
const partialPose = Speedy.Matrix.Eye(3);
|
916
|
838
|
|
917
|
839
|
// we want the estimated partial pose [ r1 | r2 | t ] to be as close
|
|
@@ -924,6 +846,7 @@ export class CameraModel
|
924
|
846
|
const rt = this._estimatePartialPose(residual); // rt should converge to the identity matrix
|
925
|
847
|
partialPose.setToSync(rt.times(partialPose));
|
926
|
848
|
residual.setToSync(residual.times(rt.inverse()));
|
|
849
|
+ //console.log("rt",rt.toString());
|
927
|
850
|
//console.log("residual",residual.toString());
|
928
|
851
|
}
|
929
|
852
|
//console.log('-----------');
|
|
@@ -937,14 +860,6 @@ export class CameraModel
|
937
|
860
|
console.log("Pose * NORMALIZED HOM^-1", result.toString());
|
938
|
861
|
*/
|
939
|
862
|
|
940
|
|
- /*
|
941
|
|
- const rt = partialPose.read();
|
942
|
|
- const r = rt.slice(0, 6);
|
943
|
|
- const t = this._refineTranslation(normalizedHomography, r, rt.slice(6, 9));
|
944
|
|
- const refinedPartialPose = Speedy.Matrix(3, 3, r.concat(t));
|
945
|
|
- const filteredPartialPose = this._filterPartialPose(refinedPartialPose);
|
946
|
|
- */
|
947
|
|
-
|
948
|
863
|
// filter the partial pose
|
949
|
864
|
const filteredPartialPose = this._filterPartialPose(partialPose);
|
950
|
865
|
|