21
21
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
22
from __future__ import annotations
23
23
24
+ from typing import NamedTuple
25
+
24
26
import numpy as np
25
27
import numpy .typing as npt
26
28
27
29
from .ippe_cf import IppeCf
28
30
from cflib .localization .lighthouse_types import LhBsCfPoses
29
31
from cflib .localization .lighthouse_types import LhCfPoseSample
32
+ from cflib .localization .lighthouse_types import LhException
30
33
from cflib .localization .lighthouse_types import Pose
31
34
32
35
36
+ ArrayFloat = npt .NDArray [np .float_ ]
37
+
38
+
39
+ class BsPairIds (NamedTuple ):
40
+ """A type representing the ids of a pair of base stations"""
41
+ bs1 : int
42
+ bs2 : int
43
+
44
+
45
+ class BsPairPoses (NamedTuple ):
46
+ """A type representing the poses of a pair of base stations"""
47
+ bs1 : Pose
48
+ bs2 : Pose
49
+
50
+
33
51
class LighthouseInitialEstimator :
34
52
"""
35
53
Make initial estimates of base station and CF poses using IPPE (analytical solution).
@@ -40,8 +58,8 @@ class LighthouseInitialEstimator:
40
58
OUTLIER_DETECTION_ERROR = 0.5
41
59
42
60
@classmethod
43
- def estimate (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : npt . ArrayLike ) -> tuple (
44
- LhBsCfPoses , list [LhCfPoseSample ]) :
61
+ def estimate (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : ArrayFloat ) -> tuple [
62
+ LhBsCfPoses , list [LhCfPoseSample ]] :
45
63
"""
46
64
Make a rough estimate of the poses of all base stations and CF poses found in the samples.
47
65
@@ -70,7 +88,7 @@ def estimate(cls, matched_samples: list[LhCfPoseSample], sensor_positions: npt.A
70
88
break
71
89
72
90
if reference_bs_pose is None :
73
- raise Exception ('Too little data, no reference' )
91
+ raise LhException ('Too little data, no reference' )
74
92
bs_poses : dict [int , Pose ] = {bs_id : reference_bs_pose }
75
93
76
94
# Calculate the pose of the remaining base stations, based on the pose of the first CF
@@ -82,8 +100,8 @@ def estimate(cls, matched_samples: list[LhCfPoseSample], sensor_positions: npt.A
82
100
return LhBsCfPoses (bs_poses , cf_poses ), cleaned_matched_samples
83
101
84
102
@classmethod
85
- def _find_solutions (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : npt . ArrayLike
86
- ) -> dict [tuple ( int , int ), npt . NDArray ]:
103
+ def _find_solutions (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : ArrayFloat
104
+ ) -> dict [BsPairIds , ArrayFloat ]:
87
105
"""
88
106
Find the pose of all base stations, in the reference frame of other base stations.
89
107
@@ -101,9 +119,9 @@ def _find_solutions(cls, matched_samples: list[LhCfPoseSample], sensor_positions
101
119
(2, 1) contains the position of base station 1, in the base station 2 reference frame.
102
120
"""
103
121
104
- position_permutations : dict [tuple ( int , int ), list [list [npt . ArrayLike ]]] = {}
122
+ position_permutations : dict [BsPairIds , list [list [ArrayFloat ]]] = {}
105
123
for sample in matched_samples :
106
- solutions : dict [int , tuple [ Pose , Pose ] ] = {}
124
+ solutions : dict [int , BsPairPoses ] = {}
107
125
for bs , angles in sample .angles_calibrated .items ():
108
126
projections = angles .projection_pair_list ()
109
127
estimates_ref_bs = IppeCf .solve (sensor_positions , projections )
@@ -115,18 +133,18 @@ def _find_solutions(cls, matched_samples: list[LhCfPoseSample], sensor_positions
115
133
return cls ._find_most_likely_positions (position_permutations )
116
134
117
135
@classmethod
118
- def _add_solution_permutations (cls , solutions : dict [int , tuple [ Pose , Pose ] ],
119
- position_permutations : dict [tuple [ int , int ], list [list [npt . ArrayLike ]]]):
136
+ def _add_solution_permutations (cls , solutions : dict [int , BsPairPoses ],
137
+ position_permutations : dict [BsPairIds , list [list [ArrayFloat ]]]):
120
138
"""
121
139
Add the possible permutations of base station positions for a sample to a collection of aggregated positions.
122
140
The aggregated collection contains base station positions in the reference frame of other base stations.
123
141
124
142
:param solutions: All possible positions of the base stations, in the reference frame of the Crazyflie in one
125
143
sample
126
- :param position_permutations: Aggregated possible solutions. A dictionary with base staion pairs as keys, mapped
127
- to lists of lists of possible positions. For instance, the entry for (2, 1) would
128
- contain a list of lists with 4 positions each, for where base station 1 might be
129
- located in the base station 2 reference frame.
144
+ :param position_permutations: Aggregated possible solutions. A dictionary with base station pairs as keys,
145
+ mapped to lists of lists of possible positions. For instance, the entry for (2, 1)
146
+ would contain a list of lists with 4 positions each, for where base station 1
147
+ might be located in the base station 2 reference frame.
130
148
"""
131
149
ids = sorted (solutions .keys ())
132
150
@@ -143,16 +161,16 @@ def _add_solution_permutations(cls, solutions: dict[int, tuple[Pose, Pose]],
143
161
pose3 = solution_i [1 ].inv_rotate_translate_pose (solution_j [0 ])
144
162
pose4 = solution_i [1 ].inv_rotate_translate_pose (solution_j [1 ])
145
163
146
- pair = (id_i , id_j )
164
+ pair = BsPairIds (id_i , id_j )
147
165
if pair not in position_permutations :
148
166
position_permutations [pair ] = []
149
167
position_permutations [pair ].append ([pose1 .translation , pose2 .translation ,
150
168
pose3 .translation , pose4 .translation ])
151
169
152
170
@classmethod
153
- def _angles_to_poses (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : npt . ArrayLike ,
154
- bs_positions : dict [tuple ( int , int ), npt . NDArray ]) -> tuple ( list [dict [int , Pose ]],
155
- list [LhCfPoseSample ]) :
171
+ def _angles_to_poses (cls , matched_samples : list [LhCfPoseSample ], sensor_positions : ArrayFloat ,
172
+ bs_positions : dict [BsPairIds , ArrayFloat ]) -> tuple [ list [dict [int , Pose ]],
173
+ list [LhCfPoseSample ]] :
156
174
"""
157
175
Estimate the base station poses in the Crazyflie reference frames, for each sample.
158
176
@@ -170,7 +188,7 @@ def _angles_to_poses(cls, matched_samples: list[LhCfPoseSample], sensor_position
170
188
cleaned_matched_samples : list [LhCfPoseSample ] = []
171
189
172
190
for sample in matched_samples :
173
- solutions : dict [int , tuple [ Pose , Pose ] ] = {}
191
+ solutions : dict [int , BsPairPoses ] = {}
174
192
for bs , angles in sample .angles_calibrated .items ():
175
193
projections = angles .projection_pair_list ()
176
194
estimates_ref_bs = IppeCf .solve (sensor_positions , projections )
@@ -180,63 +198,63 @@ def _angles_to_poses(cls, matched_samples: list[LhCfPoseSample], sensor_position
180
198
poses : dict [int , Pose ] = {}
181
199
ids = sorted (solutions .keys ())
182
200
first = ids [0 ]
201
+ is_sample_valid = True
183
202
184
203
for other in ids [1 :]:
185
- pair = (first , other )
186
- expected = bs_positions [pair ]
204
+ pair_ids = BsPairIds (first , other )
205
+ expected = bs_positions [pair_ids ]
187
206
188
- firstPose , otherPose = cls ._choose_solutions (solutions [first ], solutions [other ], expected )
189
- if firstPose is not None :
190
- poses [first ] = firstPose
191
- poses [other ] = otherPose
207
+ success , pair_poses = cls ._choose_solutions (solutions [first ], solutions [other ], expected )
208
+ if success :
209
+ poses [pair_ids . bs1 ] = pair_poses . bs1
210
+ poses [pair_ids . bs2 ] = pair_poses . bs2
192
211
else :
193
- poses = None
212
+ is_sample_valid = False
194
213
break
195
214
196
- if poses is not None :
215
+ if is_sample_valid :
197
216
result .append (poses )
198
217
cleaned_matched_samples .append (sample )
199
218
200
219
return result , cleaned_matched_samples
201
220
202
221
@classmethod
203
- def _choose_solutions (cls , solutions_1 : tuple [ Pose , Pose ], solutions_2 : tuple [ Pose , Pose ] ,
204
- expected : npt . ArrayLike ) -> tuple [Pose , Pose ]:
222
+ def _choose_solutions (cls , solutions_1 : BsPairPoses , solutions_2 : BsPairPoses ,
223
+ expected : ArrayFloat ) -> tuple [bool , BsPairPoses ]:
205
224
"""Pick the base pose solutions for a pair of base stations, based on the position in expected"""
206
225
207
226
min_dist = 100000.0
208
- best1 = None
209
- best2 = None
227
+ best = BsPairPoses ( Pose (), Pose ())
228
+ success = True
210
229
211
230
for solution_1 in solutions_1 :
212
231
for solution_2 in solutions_2 :
213
232
pose_second_bs_ref_fr_first = solution_1 .inv_rotate_translate_pose (solution_2 )
214
233
dist = np .linalg .norm (expected - pose_second_bs_ref_fr_first .translation )
215
234
if dist < min_dist :
216
235
min_dist = dist
217
- best1 = solution_1
218
- best2 = solution_2
236
+ best = BsPairPoses (solution_1 , solution_2 )
219
237
220
238
if min_dist > cls .OUTLIER_DETECTION_ERROR :
221
- return None , None
239
+ success = False
222
240
223
- return best1 , best2
241
+ return success , best
224
242
225
243
@classmethod
226
- def _find_most_likely_positions (cls , position_permutations : dict [tuple ( int , int ) ,
227
- list [list [npt . ArrayLike ]]]) -> dict [tuple ( int , int ), npt . NDArray ]:
244
+ def _find_most_likely_positions (cls , position_permutations : dict [BsPairIds ,
245
+ list [list [ArrayFloat ]]]) -> dict [BsPairIds , ArrayFloat ]:
228
246
"""
229
247
Find the most likely base station positions from all the possible permutations.
230
248
231
249
Sort the permutations into buckets based on how close they are to the solutions in the first sample. Solutions
232
- that are "too" far away and distcarded . The bucket with the most samples in, is considerred the best.
250
+ that are "too" far away and discarded . The bucket with the most samples in, is considered the best.
233
251
"""
234
- result : dict [tuple ( int , int ), npt . NDArray ] = {}
252
+ result : dict [BsPairIds , ArrayFloat ] = {}
235
253
236
254
for pair , position_lists in position_permutations .items ():
237
255
# Use first as reference to map the others to
238
256
bucket_ref_positions = position_lists [0 ]
239
- buckets : list [list [npt . NDArray ]] = [[], [], [], []]
257
+ buckets : list [list [ArrayFloat ]] = [[], [], [], []]
240
258
241
259
cls ._map_positions_to_ref (bucket_ref_positions , position_lists , buckets )
242
260
best_pos = cls ._find_best_position_bucket (buckets )
@@ -245,8 +263,9 @@ def _find_most_likely_positions(cls, position_permutations: dict[tuple(int, int)
245
263
return result
246
264
247
265
@classmethod
248
- def _map_positions_to_ref (cls , bucket_ref_positions : list [npt .ArrayLike ], position_lists : list [list [npt .ArrayLike ]],
249
- buckets : list [list [npt .ArrayLike ]]) -> None :
266
+ def _map_positions_to_ref (cls , bucket_ref_positions : list [ArrayFloat ],
267
+ position_lists : list [list [ArrayFloat ]],
268
+ buckets : list [list [ArrayFloat ]]) -> None :
250
269
"""
251
270
Sort solution into buckets based on the distance to the reference position. If no bucket is close enough,
252
271
the solution is discarded.
@@ -262,7 +281,7 @@ def _map_positions_to_ref(cls, bucket_ref_positions: list[npt.ArrayLike], positi
262
281
break
263
282
264
283
@classmethod
265
- def _find_best_position_bucket (cls , buckets : list [list [npt . ArrayLike ]]) -> npt . NDArray :
284
+ def _find_best_position_bucket (cls , buckets : list [list [ArrayFloat ]]) -> ArrayFloat :
266
285
"""
267
286
Find the bucket with the most solutions in, this is considered to be the correct solution.
268
287
The final result is the mean of the solution in the bucket.
@@ -278,7 +297,7 @@ def _find_best_position_bucket(cls, buckets: list[list[npt.ArrayLike]]) -> npt.N
278
297
return pos
279
298
280
299
@classmethod
281
- def _convert_estimates_to_cf_reference_frame (cls , estimates_ref_bs : list [IppeCf .Solution ]) -> tuple [ Pose , Pose ] :
300
+ def _convert_estimates_to_cf_reference_frame (cls , estimates_ref_bs : list [IppeCf .Solution ]) -> BsPairPoses :
282
301
"""
283
302
Convert the two ippe solutions from the base station reference frame to the CF reference frame
284
303
"""
@@ -288,12 +307,12 @@ def _convert_estimates_to_cf_reference_frame(cls, estimates_ref_bs: list[IppeCf.
288
307
rot_2 = estimates_ref_bs [1 ].R .transpose ()
289
308
t_2 = np .dot (rot_2 , - estimates_ref_bs [1 ].t )
290
309
291
- return Pose (rot_1 , t_1 ), Pose (rot_2 , t_2 )
310
+ return BsPairPoses ( Pose (rot_1 , t_1 ), Pose (rot_2 , t_2 ) )
292
311
293
312
@classmethod
294
313
def _estimate_remaining_bs_poses (cls , bs_poses_ref_cfs : list [dict [int , Pose ]], bs_poses : dict [int , Pose ]) -> None :
295
314
"""
296
- Based on one base station pose, estimate the other base staion poses.
315
+ Based on one base station pose, estimate the other base station poses.
297
316
298
317
The process is iterative and runs until all poses are found. Assume we know the pose of base station 0, and we
299
318
have information of base station pairs (0, 2) and (2, 3), from this we can first derive the pose of 2 and after
@@ -342,7 +361,7 @@ def _estimate_remaining_bs_poses(cls, bs_poses_ref_cfs: list[dict[int, Pose]], b
342
361
break
343
362
344
363
if len (to_find ) == remaining :
345
- raise RuntimeError ('Can not link positions between all base stations' )
364
+ raise LhException ('Can not link positions between all base stations' )
346
365
347
366
remaining = len (to_find )
348
367
@@ -367,7 +386,7 @@ def q_average(Q, W=None):
367
386
return Pose .from_quat (R_quat = average_quaternion , t_vec = average_pos )
368
387
369
388
@classmethod
370
- def _estimate_cf_poses (cls , bs_poses_ref_cfs : list [dict [int , Pose ]], bs_poses : list [ Pose ]) -> list [Pose ]:
389
+ def _estimate_cf_poses (cls , bs_poses_ref_cfs : list [dict [int , Pose ]], bs_poses : dict [ int , Pose ]) -> list [Pose ]:
371
390
"""
372
391
Find the pose of the Crazyflie in all samples, based on the base station poses.
373
392
"""
0 commit comments