浏览代码

Introduce the PoseFilter. Use Quaternions to improve the smoothing of rotations

customisations
alemart 10 个月前
父节点
当前提交
bb2953a580
共有 2 个文件被更改,包括 214 次插入96 次删除
  1. 26
    96
      src/geometry/camera-model.ts
  2. 188
    0
      src/geometry/pose-filter.ts

+ 26
- 96
src/geometry/camera-model.ts 查看文件

@@ -28,6 +28,7 @@ import { SpeedyVector2 } from 'speedy-vision/types/core/speedy-vector';
28 28
 import { SpeedyPromise } from 'speedy-vision/types/core/speedy-promise';
29 29
 import { Nullable, Utils } from '../utils/utils';
30 30
 import { Settings } from '../core/settings';
31
+import { PoseFilter } from './pose-filter';
31 32
 import { IllegalOperationError, IllegalArgumentError } from '../utils/errors';
32 33
 
33 34
 /** A guess of the horizontal field-of-view of a typical camera, in degrees */
@@ -36,12 +37,6 @@ const HFOV_GUESS = 60; // https://developer.apple.com/library/archive/documentat
36 37
 /** Number of iterations used to refine the estimated pose */
37 38
 const POSE_ITERATIONS = 30;
38 39
 
39
-/** Number of samples used in the rotation filter */
40
-const ROTATION_FILTER_SAMPLES = 10;
41
-
42
-/** Number of samples used in the translation filter */
43
-const TRANSLATION_FILTER_SAMPLES = 5;
44
-
45 40
 /** Convert degrees to radians */
46 41
 const DEG2RAD = 0.017453292519943295; // pi / 180
47 42
 
@@ -83,11 +78,8 @@ export class CameraModel
83 78
     /** extrinsics matrix, in column-major format */
84 79
     private _extrinsics: number[];
85 80
 
86
-    /** filter: samples of partial rotation matrix [ r1 | r2 ] */
87
-    private _partialRotationBuffer: number[][];
88
-
89
-    /** filter: samples of translation vector t */
90
-    private _translationBuffer: number[][];
81
+    /** smoothing filter */
82
+    private _filter: PoseFilter;
91 83
 
92 84
 
93 85
 
@@ -100,8 +92,7 @@ export class CameraModel
100 92
         this._matrix = Speedy.Matrix.Eye(3, 4);
101 93
         this._intrinsics = [1,0,0,0,1,0,0,0,1]; // 3x3 identity matrix
102 94
         this._extrinsics = [1,0,0,0,1,0,0,0,1,0,0,0]; // 3x4 matrix [ R | t ] = [ I | 0 ] no rotation & no translation
103
-        this._partialRotationBuffer = [];
104
-        this._translationBuffer = [];
95
+        this._filter = new PoseFilter();
105 96
     }
106 97
 
107 98
     /**
@@ -177,12 +168,13 @@ export class CameraModel
177 168
 
178 169
         // estimate the pose
179 170
         const pose = this._estimatePose(homography);
180
-        this._extrinsics = pose.read();
171
+        if(this._filter.feed(pose))
172
+            this._extrinsics = this._filter.run().read();
181 173
 
182 174
         // compute the camera matrix
183 175
         const C = this.denormalizer();
184 176
         const K = Speedy.Matrix(3, 3, this._intrinsics);
185
-        const E = pose; //Speedy.Matrix(3, 4, this._extrinsics);
177
+        const E = Speedy.Matrix(3, 4, this._extrinsics);
186 178
         this._matrix.setToSync(K.times(E).times(C));
187 179
         //console.log("intrinsics -----------", K.toString());
188 180
         //console.log("matrix ----------------",this._matrix.toString());
@@ -320,9 +312,8 @@ export class CameraModel
320 312
         this._extrinsics.fill(0);
321 313
         this._extrinsics[0] = this._extrinsics[4] = this._extrinsics[8] = 1;
322 314
 
323
-        // reset filters
324
-        this._partialRotationBuffer.length = 0;
325
-        this._translationBuffer.length = 0;
315
+        // reset filter
316
+        this._filter.reset();
326 317
     }
327 318
 
328 319
     /**
@@ -755,72 +746,15 @@ export class CameraModel
755 746
     }
756 747
 
757 748
     /**
758
-     * Apply a smoothing filter to the partial pose
759
-     * @param partialPose 3x3 [ r1 | r2 | t ]
760
-     * @returns filtered partial pose
761
-     */
762
-    private _filterPartialPose(partialPose: SpeedyMatrix): SpeedyMatrix
763
-    {
764
-        const avg: number[] = new Array(9).fill(0);
765
-        const entries = partialPose.read();
766
-        const rotationBlock = entries.slice(0, 6);
767
-        const translationBlock = entries.slice(6, 9);
768
-
769
-        // how many samples should we store, at most?
770
-        const div = (Settings.powerPreference == 'low-power') ? 1.5 : 1; // low-power ~ half the fps
771
-        const N = Math.ceil(ROTATION_FILTER_SAMPLES / div);
772
-        const M = Math.ceil(TRANSLATION_FILTER_SAMPLES / div);
773
-
774
-        // is it a valid partial pose?
775
-        if(!Number.isNaN(entries[0])) {
776
-            // store samples
777
-            this._partialRotationBuffer.unshift(rotationBlock);
778
-            if(this._partialRotationBuffer.length > N)
779
-                this._partialRotationBuffer.length = N;
780
-
781
-            this._translationBuffer.unshift(translationBlock);
782
-            if(this._translationBuffer.length > M)
783
-                this._translationBuffer.length = M;
784
-        }
785
-        else if(this._partialRotationBuffer.length == 0) {
786
-            // invalid pose, no samples
787
-            return Speedy.Matrix.Eye(3);
788
-        }
789
-
790
-        // average *nearby* rotations
791
-        const n = this._partialRotationBuffer.length;
792
-        for(let i = 0; i < n; i++) {
793
-            const r = this._partialRotationBuffer[i];
794
-            for(let j = 0; j < 6; j++)
795
-                avg[j] += r[j] / n;
796
-        }
797
-        const r = this._refineRotation(avg);
798
-
799
-        // average translations
800
-        const m = this._translationBuffer.length;
801
-        for(let i = 0; i < m; i++) {
802
-            const t = this._translationBuffer[i];
803
-            for(let j = 0; j < 3; j++)
804
-                avg[6 + j] += (m - i) * t[j] / ((m * m + m) / 2);
805
-                //avg[6 + j] += t[j] / m;
806
-        }
807
-        const t = [ avg[6], avg[7], avg[8] ];
808
-
809
-        // done!
810
-        return Speedy.Matrix(3, 3, r.concat(t));
811
-    }
812
-
813
-    /**
814
-     * Estimate extrinsics [ R | t ] given a partial pose [ r1 | r2 | t ]
815
-     * @param partialPose
816
-     * @returns 3x4 matrix
749
+     * Find a 3x3 rotation matrix R given two orthonormal vectors [ r1 | r2 ]
750
+     * @param partialRotation partial rotation matrix [ r1 | r2 ] in column-major format
751
+     * @returns a rotation matrix R in column-major format
817 752
      */
818
-    private _estimateFullPose(partialPose: SpeedyMatrix): SpeedyMatrix
753
+    private _computeFullRotation(partialRotation: number[]): number[]
819 754
     {
820
-        const p = partialPose.read();
821
-        const r11 = p[0], r12 = p[3], t1 = p[6];
822
-        const r21 = p[1], r22 = p[4], t2 = p[7];
823
-        const r31 = p[2], r32 = p[5], t3 = p[8];
755
+        const r11 = partialRotation[0], r12 = partialRotation[3];
756
+        const r21 = partialRotation[1], r22 = partialRotation[4];
757
+        const r31 = partialRotation[2], r32 = partialRotation[5];
824 758
 
825 759
         // r3 = +- ( r1 x r2 )
826 760
         let r13 = r21 * r32 - r31 * r22;
@@ -836,12 +770,11 @@ export class CameraModel
836 770
         }
837 771
 
838 772
         // done!
839
-        return Speedy.Matrix(3, 4, [
773
+        return [
840 774
             r11, r21, r31,
841 775
             r12, r22, r32,
842
-            r13, r23, r33,
843
-            t1, t2, t3,           
844
-        ]);
776
+            r13, r23, r33
777
+        ];
845 778
     }
846 779
 
847 780
     /**
@@ -870,19 +803,16 @@ export class CameraModel
870 803
         }
871 804
         //console.log('-----------');
872 805
 
873
-        // refine the translation vector
806
+        // read the partial pose
874 807
         const mat = partialPose.read();
875
-        const r = mat.slice(0, 6);
808
+        const r0 = mat.slice(0, 6);
876 809
         const t0 = mat.slice(6, 9);
877
-        const t = this._refineTranslation(normalizedHomography, r, t0);
878
-        const refinedPartialPose = Speedy.Matrix(3, 3, r.concat(t));
879 810
 
880
-        // filter the partial pose
881
-        const filteredPartialPose = this._filterPartialPose(refinedPartialPose);
811
+        // refine the translation vector and compute the full rotation matrix
812
+        const t = this._refineTranslation(normalizedHomography, r0, t0);
813
+        const r = this._computeFullRotation(r0);
882 814
 
883
-        // estimate the full pose
884
-        //const finalPartialPose = partialPose;
885
-        const finalPartialPose = filteredPartialPose;
886
-        return this._estimateFullPose(finalPartialPose);
815
+        // done!
816
+        return Speedy.Matrix(3, 4, r.concat(t));
887 817
     }
888 818
 }

+ 188
- 0
src/geometry/pose-filter.ts 查看文件

@@ -0,0 +1,188 @@
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
+ * pose-filter.ts
20
+ * Smoothing filter for a pose
21
+ */
22
+
23
+import Speedy from 'speedy-vision';
24
+import { SpeedyMatrix } from 'speedy-vision/types/core/speedy-matrix';
25
+import { Settings } from '../core/settings';
26
+import { Quaternion } from './quaternion';
27
+import { Vector3 } from './vector3';
28
+import { IllegalArgumentError } from '../utils/errors';
29
+
30
+/** Number of translation samples */
31
+const TRANSLATION_SAMPLES = 5;
32
+
33
+/** Number of rotation samples */
34
+const ROTATION_SAMPLES = 12;
35
+
36
+/** A vector representing "no translation" */
37
+const NO_TRANSLATION = Vector3.Zero();
38
+
39
+/** A quaternion representing "no rotation" */
40
+const NO_ROTATION = Quaternion.Identity();
41
+
42
+/** The zero quaternion */
43
+const ZERO_QUATERNION = new Quaternion(0, 0, 0, 0);
44
+
45
+
46
+
47
+
48
+
49
+/**
50
+ * Smoothing filter for a pose
51
+ */
52
+export class PoseFilter
53
+{
54
+    /** smooth rotation */
55
+    private _smoothRotation: Quaternion;
56
+
57
+    /** smooth translation */
58
+    private _smoothTranslation: Vector3;
59
+
60
+    /** samples of rotations */
61
+    private _rotationSample: Quaternion[];
62
+
63
+    /** samples of translations */
64
+    private _translationSample: Vector3[];
65
+
66
+    /** empty buffers? (i.e., no samples collected?) */
67
+    private _isEmpty: boolean;
68
+
69
+
70
+
71
+    /**
72
+     * Constructor
73
+     */
74
+    constructor()
75
+    {
76
+        this._smoothRotation = Quaternion.Identity();
77
+        this._smoothTranslation = Vector3.Zero();
78
+        this._rotationSample = Array.from({ length: ROTATION_SAMPLES }, () => Quaternion.Identity());
79
+        this._translationSample = Array.from({ length: TRANSLATION_SAMPLES }, () => Vector3.Zero());
80
+        this._isEmpty = true;
81
+    }
82
+
83
+    /**
84
+     * Reset the filter
85
+     */
86
+    reset(): void
87
+    {
88
+        this._rotationSample.forEach(q => q._copyFrom(NO_ROTATION));
89
+        this._translationSample.forEach(t => t._copyFrom(NO_TRANSLATION));
90
+        this._isEmpty = true;
91
+    }
92
+
93
+    /**
94
+     * Feed the filter with a sample
95
+     * @param sample 3x4 [ R | t ] matrix
96
+     * @returns true on success
97
+     */
98
+    feed(sample: SpeedyMatrix): boolean
99
+    {
100
+        const data = sample.read();
101
+
102
+        // sanity check
103
+        if(sample.rows != 3 || sample.columns != 4)
104
+            throw new IllegalArgumentError();
105
+
106
+        // discard invalid samples
107
+        if(Number.isNaN(data[0] * data[9])) // rotation, translation
108
+            return false;
109
+
110
+        // store sample
111
+        const q = this._rotationSample[ROTATION_SAMPLES - 1];
112
+        for(let i = ROTATION_SAMPLES - 1; i > 0; i--)
113
+            this._rotationSample[i] = this._rotationSample[i-1];
114
+        this._rotationSample[0] = q._fromRotationMatrix(sample.block(0, 2, 0, 2));
115
+
116
+        const t = this._translationSample[TRANSLATION_SAMPLES - 1];
117
+        for(let i = TRANSLATION_SAMPLES - 1; i > 0; i--)
118
+            this._translationSample[i] = this._translationSample[i-1];
119
+        this._translationSample[0] = t._set(data[9], data[10], data[11]);
120
+
121
+        // empty buffers?
122
+        if(this._isEmpty) {
123
+            this._rotationSample.forEach((q, i) => i > 0 && q._copyFrom(this._rotationSample[0]));
124
+            this._translationSample.forEach((t, i) => i > 0 && t._copyFrom(this._translationSample[0]));
125
+            this._isEmpty = false;
126
+        }
127
+
128
+        // done!
129
+        return true;
130
+    }
131
+
132
+    /**
133
+     * Run the filter
134
+     * @returns a 3x4 [ R | t ] matrix
135
+     */
136
+    run(): SpeedyMatrix
137
+    {
138
+        // how many samples should we use, at most?
139
+        const div = (Settings.powerPreference == 'low-power') ? 1.5 : 1; // low-power ~ half the fps
140
+        const T = Math.ceil(TRANSLATION_SAMPLES / div);
141
+        const R = Math.ceil(ROTATION_SAMPLES / div);
142
+
143
+        // clear the output of the filter
144
+        const t = this._smoothTranslation._copyFrom(NO_TRANSLATION);
145
+        const q = this._smoothRotation._copyFrom(ZERO_QUATERNION);
146
+
147
+        // average translations
148
+        for(let i = 0, d = 2 / (T * T + T); i < T; i++) {
149
+            const ti = this._translationSample[i];
150
+            const w = (T - i) * d;
151
+
152
+            // weighted avg: sum from i=0 to T-1 { (T-i) * t[i] } * (2/(T^2+T))
153
+            t.x += ti.x * w;
154
+            t.y += ti.y * w;
155
+            t.z += ti.z * w;
156
+        }
157
+
158
+        // average *nearby* rotations
159
+        // based on https://web.archive.org/web/20130514122622/http://wiki.unity3d.com/index.php/Averaging_Quaternions_and_Vectors
160
+        for(let i = 0; i < R; i++) {
161
+            const qi = this._rotationSample[i];
162
+            const w = 1 / R; //(R - (i - i%2)) / R;
163
+
164
+            // since unit quaternions qi and -qi encode the same rotation
165
+            // (see quaternion.ts), let's enforce dot(qi, 1) = qi.w >= 0
166
+            if(qi.w < 0) {
167
+                // XXX since Quaternion._fromRotationMatrix() computes w >= 0,
168
+                // this will never happen. Leave this here for extra safety
169
+                // in case anything changes?
170
+                qi.x = -qi.x;
171
+                qi.y = -qi.y;
172
+                qi.z = -qi.z;
173
+                qi.w = -qi.w;
174
+            }
175
+
176
+            q.x += qi.x * w;
177
+            q.y += qi.y * w;
178
+            q.z += qi.z * w;
179
+            q.w += qi.w * w;
180
+        }
181
+        //q._normalize();
182
+
183
+        // convert to matrix form and return
184
+        const entries = q._toRotationMatrix().read();
185
+        entries.push(t.x, t.y, t.z);
186
+        return Speedy.Matrix(3, 4, entries);
187
+    }
188
+}

正在加载...
取消
保存