24
24
warnings .filterwarnings (action = 'ignore' , category = UserWarning , message = 'Gimbal lock detected.' )
25
25
26
26
from xml .etree import ElementTree
27
- from typing import Optional
27
+ from typing import Optional , Tuple
28
28
from dataclasses import dataclass , field
29
29
from collections import namedtuple
30
- from multiprocessing .pool import AsyncResult
31
30
from scipy .spatial .transform import Rotation
32
- from geopandas import GeoSeries
33
31
from shapely .geometry import box
32
+ from geographic_msgs .msg import GeoPoint
34
33
35
34
from gisnav .assertions import assert_type , assert_ndim , assert_shape , assert_len
36
35
from gisnav .geo import GeoPt , GeoTrapezoid , GeoValueError
37
36
38
- from geographic_msgs .msg import GeoPoint as ROSGeoPoint
39
-
40
37
Dim = namedtuple ('Dim' , 'height width' )
41
38
TimePair = namedtuple ('TimePair' , 'local foreign' )
42
39
BBox = namedtuple ('BBox' , 'left bottom right top' )
@@ -50,7 +47,7 @@ class Position:
50
47
.. note::
51
48
(x, y, z) coordinates are in ENU frame
52
49
"""
53
- xy : GeoPt # XY coordinates (e.g. longitude & latitude in WGS84)
50
+ xy : GeoPt # XY coordinates (e.g. longitude & latitude in WGS84)
54
51
altitude : Altitude
55
52
attitude : Optional [Attitude ] # attitude in NED frame
56
53
timestamp : Optional [int ] # Reference timestamp of position
@@ -95,7 +92,7 @@ def to_esd(self) -> Attitude:
95
92
96
93
:return: Attitude in SED frame
97
94
"""
98
- nadir_pitch = np .array ([0 , np .sin (np .pi / 4 ), 0 , np .sin (np .pi / 4 )]) # Adjust origin to nadir facing camera
95
+ nadir_pitch = np .array ([0 , np .sin (np .pi / 4 ), 0 , np .sin (np .pi / 4 )]) # Adjust origin to nadir facing camera
99
96
r = Rotation .from_quat (self .q ) * Rotation .from_quat (nadir_pitch )
100
97
q = r .as_quat ()
101
98
q = np .array ([q [1 ], - q [0 ], q [2 ], - q [3 ]]) # NED to ESD
@@ -163,40 +160,40 @@ def __post_init__(self):
163
160
@dataclass (frozen = True )
164
161
class MapData (_ImageHolder ):
165
162
"""Keeps map frame related data in one place and protects it from corruption."""
166
- #bbox: GeoSquare
167
163
bbox : BBox
168
164
elevation : Optional [Img ] = None # Optional elevation raster
169
165
170
166
def __post_init__ (self ):
171
167
"""Post-initialization validity check."""
172
168
if self .elevation is not None :
173
169
assert self .elevation .arr .shape [0 :2 ], self .image .arr .shape [0 :2 ]
174
- assert_ndim (self .elevation .arr , 2 ) # Grayscale image expected
170
+ assert_ndim (self .elevation .arr , 2 ) # Grayscale image expected
175
171
176
172
177
173
# noinspection PyClassHasNoInit
178
174
@dataclass (frozen = True )
179
175
class ContextualMapData (_ImageHolder ):
180
- """Contains the rotated and cropped map image for _match estimation"""
181
- image : Img = field (init = False ) # This is the cropped and rotated map which is same size as the camera frames
176
+ """Contains the rotated and cropped map image pose estimation"""
177
+ image : Img = field (init = False ) # Cropped and rotated map which is the same size as the camera frames
182
178
elevation : Optional [Img ] = field (init = None ) # Rotated elevation raster (optional) in meters
183
- rotation : float # radians
184
- crop : Dim # Same value will also be found at image.dim (but not at initialization)
185
- map_data : MapData # This is the original (square) map with padding
179
+ rotation : float # radians
180
+ crop : Dim # Same value will also be found at image.dim (but not at initialization)
181
+ map_data : MapData # This is the original (square) map with padding
186
182
pix_to_wgs84 : np .ndarray = field (init = False )
187
- mock_data : bool = False # Indicates that this was used for field of view guess (mock map data)
188
- altitude_scaling : Optional [float ] = None # altitude scaling (elevation raster meters -> camera pixels)
183
+ mock_data : bool = False # Indicates that this was used for field of view guess (mock map data)
184
+ altitude_scaling : Optional [float ] = None # altitude scaling (elevation raster meters -> camera pixels)
189
185
186
+ # TODO: update docs - only one transformation is returned
190
187
def _pix_to_wgs84 (self ) -> Tuple [np .ndarray , np .ndarray , np .ndarray , np .ndarray ]:
191
- """Returns tuple of affine 2D transformation matrix for converting matched pixel coordinates to WGS84 coordinates
192
- along with intermediate transformations
188
+ """Returns tuple of affine 2D transformation matrix for converting matched pixel coordinates to WGS84
189
+ coordinates along with intermediate transformations
193
190
194
- These transformations can be used to reverse the rotation and cropping that :func:`~rotate_and_crop_map ` did to
191
+ These transformations can be used to reverse the rotation and cropping that :func:`._rotate_and_crop_map ` did to
195
192
the original map.
196
193
197
- :return: Tuple containing 2D affinre transformations from 1. pixel coordinates to WGS84, 2. from original unrotated
198
- and uncropped map pixel coordinates to WGS84, 3. from rotated map coordinates to unrotated map coordinates, and 4.
199
- from cropped map coordinates to uncropped (but still rotated) map pixel coordinates.
194
+ :return: Tuple containing 2D affine transformations from 1. pixel coordinates to WGS84, 2. from original
195
+ unrotated and uncropped map pixel coordinates to WGS84, 3. from rotated map coordinates to unrotated map
196
+ coordinates, and 4. from cropped map coordinates to uncropped (but still rotated) map pixel coordinates.
200
197
"""
201
198
map_dim_arr = np .array (self .map_data .image .dim )
202
199
img_dim_arr = np .array (self .image .dim )
@@ -212,20 +209,16 @@ def _pix_to_wgs84(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
212
209
uncropped_to_unrotated = np .vstack ((rotation , rotation_padding ))
213
210
214
211
src_corners = create_src_corners (* self .map_data .image .dim )
215
- #dst_corners = self.map_data.bbox.to_crs('epsg:4326').coords # .reshape(-1, 1, 2)
216
212
coords = np .array (box (* self .map_data .bbox ).exterior .coords )
217
213
gt = GeoTrapezoid (coords ) # .reshape(-1, 1, 2)
218
- #dst_corners = np.flip(coords[:-1], axis=1) # from ENU frame to WGS 84 axis order
219
214
dst_corners = gt .square_coords
220
215
dst_corners = np .flip (dst_corners , axis = 1 ) # from ENU frame to WGS 84 axis order
221
216
unrotated_to_wgs84 = cv2 .getPerspectiveTransform (np .float32 (src_corners ).squeeze (),
222
217
np .float32 (dst_corners ).squeeze ())
223
218
224
219
# Ratio of boundaries in pixels and meters -> Altitude (z) scaling factor
225
- #vertical_scaling = abs(self.map_data.bbox.meter_length / \
226
- # (2*self.map_data.image.dim.width + 2*self.map_data.image.dim.height))
227
220
vertical_scaling = abs (gt .meter_length / \
228
- (2 * self .map_data .image .dim .width + 2 * self .map_data .image .dim .height ))
221
+ (2 * self .map_data .image .dim .width + 2 * self .map_data .image .dim .height ))
229
222
230
223
pix_to_wgs84_ = unrotated_to_wgs84 @ uncropped_to_unrotated @ pix_to_uncropped
231
224
@@ -234,9 +227,7 @@ def _pix_to_wgs84(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
234
227
return pix_to_wgs84_ # , unrotated_to_wgs84, uncropped_to_unrotated, pix_to_uncropped
235
228
236
229
def _rotate_and_crop_map (self , elevation : bool = False ) -> np .ndarray :
237
- """Rotates map counter-clockwise and then crops a dimensions-sized part from the middle.
238
-
239
- Map needs padding so that a circle with diameter of the diagonal of the img_size rectangle is enclosed in map.
230
+ """Rotates map counter-clockwise and then crops a dimensions-sized part from the middle
240
231
241
232
:param elevation: Set True to do rotation on elevation raster instead
242
233
:return: Rotated and cropped map raster
@@ -260,7 +251,7 @@ def _rotate_and_crop_map(self, elevation: bool = False) -> np.ndarray:
260
251
261
252
@staticmethod
262
253
def _crop_center (img : np .ndarray , dimensions : Dim ) -> np .ndarray :
263
- """Crops dimensions sized part from center.
254
+ """Crops dimensions sized part from center
264
255
265
256
:param img: Image to crop
266
257
:param dimensions: Dimensions of area to crop (not of image itself)
@@ -269,9 +260,8 @@ def _crop_center(img: np.ndarray, dimensions: Dim) -> np.ndarray:
269
260
cx , cy = tuple (np .array (img .shape [0 :2 ]) / 2 )
270
261
img_cropped = img [math .floor (cy - dimensions .height / 2 ):math .floor (cy + dimensions .height / 2 ),
271
262
math .floor (cx - dimensions .width / 2 ):math .floor (cx + dimensions .width / 2 )]
272
- assert (
273
- img_cropped .shape [0 :2 ] == dimensions .height , dimensions .width ), 'Something went wrong when cropping the ' \
274
- 'map raster. '
263
+ assert (img_cropped .shape [0 :2 ] == dimensions .height , dimensions .width ), \
264
+ 'Something went wrong when cropping the map raster.'
275
265
return img_cropped
276
266
277
267
def __post_init__ (self ):
@@ -319,21 +309,6 @@ def __iter__(self):
319
309
yield item
320
310
321
311
322
- # noinspection PyClassHasNoInit
323
- @dataclass (frozen = True )
324
- class Snapshot :
325
- """Snapshot of vehicle and environment state"""
326
- synchronized_time : Optional [int ]
327
- attitude : Optional [Attitude ]
328
- gimbal_attitude : Optional [Attitude ]
329
- gimbal_set_attitude : Optional [Attitude ]
330
- global_position : Optional [GeoPt ]
331
- altitude : Optional [Altitude ]
332
- home_altitude : Optional [Altitude ]
333
- terrain_altitude : Optional [Altitude ]
334
- local_frame_origin : Optional [Position ]
335
-
336
-
337
312
# noinspection PyClassHasNoInit
338
313
@dataclass (frozen = True )
339
314
class Altitude :
@@ -345,7 +320,7 @@ class Altitude:
345
320
home: Above home or starting location
346
321
347
322
.. note::
348
- Altitude AGL should always be known (cannot be None)
323
+ TODO: Altitude AGL should always be known (cannot be None)
349
324
350
325
.. seealso::
351
326
`Altitude definitions <https://ardupilot.org/copter/docs/common-understanding-altitude.html>`_
@@ -355,24 +330,12 @@ class Altitude:
355
330
ellipsoid : Optional [float ]
356
331
home : Optional [float ]
357
332
333
+ # TODO
358
334
#def __post_init__(self):
359
335
# """Post-initialization validity checks"""
360
336
# assert self.agl is not None
361
337
362
338
363
-
364
-
365
- # noinspection PyClassHasNoInit
366
- @dataclass (frozen = True )
367
- class InputData :
368
- """InputData of vehicle state and other variables needed for postprocessing pose estimates"""
369
- r_guess : Optional [np .ndarray ]
370
- """Camera expected attitude"""
371
-
372
- snapshot : Snapshot
373
- """Snapshot of vehicle and environment state"""
374
-
375
-
376
339
# noinspection PyClassHasNoInit
377
340
@dataclass
378
341
class FOV :
@@ -418,16 +381,16 @@ def __post_init__(self):
418
381
class FixedCamera :
419
382
"""WGS84-fixed camera attributes
420
383
421
- Collects field of view and map_match under a single structure that is intended to be stored in input data context as
422
- visual odometry fix reference. Includes the needed map_match and pix_to_wgs84 transformation for the vo fix.
384
+ # TODO: refactor this class out - it was used earlier for visual odometry but is now redundant
385
+
386
+ Collects field of view and map_match under a single structure.
423
387
"""
424
388
image_pair : ImagePair
425
389
pose : Pose
426
390
timestamp : int
427
- #snapshot: Snapshot
428
391
terrain_altitude_amsl : Optional [float ]
429
392
terrain_altitude_ellipsoid : Optional [float ]
430
- home_position : Optional [ROSGeoPoint ]
393
+ home_position : Optional [GeoPoint ]
431
394
fov : FOV = field (init = False )
432
395
position : Position = field (init = False )
433
396
h : np .ndarray = field (init = False )
@@ -459,7 +422,7 @@ def _estimate_fov(self) -> Optional[FOV]:
459
422
# Could not create a valid FOV
460
423
return None
461
424
462
- def _estimate_attitude (self ) -> np . ndarray :
425
+ def _estimate_attitude (self ) -> Attitude :
463
426
"""Estimates gimbal (not vehicle) attitude in NED frame
464
427
465
428
.. note::
@@ -473,7 +436,6 @@ def _estimate_attitude(self) -> np.ndarray:
473
436
assert not np .isnan (rT ).any ()
474
437
gimbal_estimated_attitude = Rotation .from_matrix (rT ) # rotated map pixel frame
475
438
476
- #gimbal_estimated_attitude *= Rotation.from_rotvec(-(np.pi/2) * np.array([0, 1, 0])) # camera body
477
439
gimbal_estimated_attitude *= Rotation .from_rotvec (
478
440
self .image_pair .ref .rotation * np .array ([0 , 0 , 1 ])) # unrotated map pixel frame
479
441
@@ -489,7 +451,7 @@ def _get_fov_and_c(img_arr_shape: Tuple[int, int], h_mat: np.ndarray) -> Tuple[n
489
451
490
452
:param img_arr_shape: Image array shape tuple (height, width)
491
453
:param h_mat: Homography matrix
492
- :return: Tuple of FOV corner coordinates and prinicpal point np.ndarrays
454
+ :return: Tuple of FOV corner coordinates and principal point np.ndarrays
493
455
"""
494
456
assert_type (img_arr_shape , tuple )
495
457
assert_len (img_arr_shape , 2 )
@@ -519,13 +481,12 @@ def _estimate_position(self, terrain_altitude_amsl: Optional[float], terrain_alt
519
481
:param terrain_altitude_amsl: Optional ground elevation above AMSL in meters
520
482
:param terrain_altitude_ellipsoid: Optional ground elevation above WGS 84 ellipsoid in meters
521
483
:param crs: CRS to use for the Position
522
- :return: Camera position
484
+ :return: Camera position or None if not available
523
485
"""
524
486
assert self .fov is not None # Call _estimate_fov before _estimate_position!
525
487
# Translation in WGS84 (and altitude or z-axis translation in meters above ground)
526
488
assert_type (self .image_pair .ref , ContextualMapData ) # need pix_to_wgs84
527
489
t_wgs84 = self .image_pair .ref .pix_to_wgs84 @ np .append (self .camera_position [0 :2 ], 1 )
528
- #t_wgs84[2] = -self.image_pair.ref.pix_to_wgs84[2][2] * self.camera_position[2] # In NED frame z-coordinate is negative above ground, make altitude >0
529
490
t_wgs84 [2 ] = - self .fov .scaling * self .camera_position [2 ] # In NED frame z-coordinate is negative above ground, make altitude >0
530
491
531
492
# Check that we have all the values needed for a global position
@@ -560,9 +521,6 @@ def __post_init__(self):
560
521
raise DataValueError ('Please provide valid image pair.' )
561
522
562
523
img = self .image_pair .qry
563
- #if self.snapshot.terrain_altitude.amsl is not None and self.snapshot.terrain_altitude.ellipsoid is None or \
564
- # self.snapshot.terrain_altitude.amsl is not None and self.snapshot.terrain_altitude.ellipsoid is None:
565
- # raise DataValueError('Please provide terrain altitude in both AMSL and above WGS 84 ellipsoid.')
566
524
if self .terrain_altitude_amsl is not None and self .terrain_altitude_ellipsoid is None or \
567
525
self .terrain_altitude_amsl is not None and self .terrain_altitude_ellipsoid is None :
568
526
raise DataValueError ('Please provide terrain altitude in both AMSL and above WGS 84 ellipsoid.' )
0 commit comments