Skip to content

Commit 06e4612

Browse files
committed
docs: Update docstrings, minor coding style changes and removing dead code.
1 parent 4b892a7 commit 06e4612

21 files changed

+205
-232
lines changed

docs/pages/developer_guide/integrate/pose_estimators.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ You can use the below snippets to get started with your own :class:`.PoseEstimat
5353
5454
import numpy as np
5555
from typing import Optional, Tuple
56-
from gisnav.pose_estimators.pose_estimator import PoseEstimator
56+
from gisnav.pose_estimators import PoseEstimator
5757
5858
class MyPoseEstimator(PoseEstimator):
5959
@@ -86,7 +86,7 @@ You can use the below snippets to get started with your own :class:`.PoseEstimat
8686
8787
import numpy as np
8888
from typing import Optional, Tuple
89-
from gisnav.pose_estimators.keypoint_pose_estimator import KeypointPoseEstimator
89+
from gisnav.pose_estimators import KeypointPoseEstimator
9090
9191
class MyKeypointPoseEstimator(KeypointPoseEstimator):
9292

docs/pages/developer_guide/launch/run_individual_nodes.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Run individual node
22
____________________________________________________
3-
It is recommended that you :ref:`Launch GISNav using ROS launch files`. However, you can also launch individual nodes
3+
It is recommended that you :ref:`Launch from ROS launch file`. However, you can also launch individual nodes
44
and optionally provide launch values for ROS parameters from the YAML files in the ``launch/params/`` folder:
55

66
.. code-block:: bash

gisnav/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""ROS 2 package for estimating airborne drone global position by matching video to map retrieved from onboard GIS server."""

gisnav/assertions.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Common assertions for convenience"""
2-
from typing import Optional, Any, Union, Sequence, Collection, Tuple, get_args
2+
from typing import Any, Union, Sequence, Collection, Tuple
33

44
import numpy as np
55

@@ -9,7 +9,6 @@ def assert_type(value: object, type_: Any) -> None:
99
1010
:param value: Object to check
1111
:param type_: Type to be asserted
12-
:return:
1312
"""
1413
assert isinstance(value, type_), f'Type {type(value)} provided when {type_} was expected.'
1514

@@ -19,43 +18,39 @@ def assert_ndim(value: np.ndarray, ndim: int) -> None:
1918
2019
:param value: Numpy array to check
2120
:param ndim: Required number of dimensions
22-
:return:
2321
"""
2422
assert value.ndim == ndim, f'Unexpected number of dimensions: {value.ndim} ({ndim} expected).'
2523

2624

27-
def assert_len(value: Union[Sequence, Collection], len_: int):
25+
def assert_len(value: Union[Sequence, Collection], len_: int) -> None:
2826
"""Asserts a specific length for a sequence or a collection (e.g. a list).
2927
3028
:param value: Sequence or collection to check
3129
:param len_: Required length
32-
:return:
3330
"""
3431
assert len(value) == len_, f'Unexpected length: {len(value)} ({len_} expected).'
3532

3633

37-
def assert_shape(value: np.ndarray, shape: tuple):
34+
def assert_shape(value: np.ndarray, shape: tuple) -> None:
3835
"""Asserts a specific shape for np.ndarray.
3936
4037
:param value: Numpy array to check
4138
:param shape: Required shape
42-
:return:
4339
"""
4440
assert value.shape == shape, f'Unexpected shape: {value.shape} ({shape} expected).'
4541

4642

47-
def assert_pose(pose: Tuple[np.ndarray, np.ndarray]):
43+
def assert_pose(pose: Tuple[np.ndarray, np.ndarray]) -> None:
4844
"""Asserts that provided tuple is a valid pose (r, t)
4945
5046
:param pose: Tuple consisting of a rotation (3, 3) and translation (3, 1) numpy arrays with valid values
51-
:return: True if pose tuple is correctly formatted and valid
5247
"""
5348
r, t = pose
5449
assert_rotation_matrix(r)
5550
assert_shape(t, (3, 1))
5651

5752

58-
def assert_rotation_matrix(r: np.ndarray):
53+
def assert_rotation_matrix(r: np.ndarray) -> None:
5954
"""Asserts that matrix is a valid rotation matrix
6055
6156
Provided matrix of shape (3, 3) with valid values

gisnav/data.py

Lines changed: 33 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,16 @@
2424
warnings.filterwarnings(action='ignore', category=UserWarning, message='Gimbal lock detected.')
2525

2626
from xml.etree import ElementTree
27-
from typing import Optional
27+
from typing import Optional, Tuple
2828
from dataclasses import dataclass, field
2929
from collections import namedtuple
30-
from multiprocessing.pool import AsyncResult
3130
from scipy.spatial.transform import Rotation
32-
from geopandas import GeoSeries
3331
from shapely.geometry import box
32+
from geographic_msgs.msg import GeoPoint
3433

3534
from gisnav.assertions import assert_type, assert_ndim, assert_shape, assert_len
3635
from gisnav.geo import GeoPt, GeoTrapezoid, GeoValueError
3736

38-
from geographic_msgs.msg import GeoPoint as ROSGeoPoint
39-
4037
Dim = namedtuple('Dim', 'height width')
4138
TimePair = namedtuple('TimePair', 'local foreign')
4239
BBox = namedtuple('BBox', 'left bottom right top')
@@ -50,7 +47,7 @@ class Position:
5047
.. note::
5148
(x, y, z) coordinates are in ENU frame
5249
"""
53-
xy: GeoPt # XY coordinates (e.g. longitude & latitude in WGS84)
50+
xy: GeoPt # XY coordinates (e.g. longitude & latitude in WGS84)
5451
altitude: Altitude
5552
attitude: Optional[Attitude] # attitude in NED frame
5653
timestamp: Optional[int] # Reference timestamp of position
@@ -95,7 +92,7 @@ def to_esd(self) -> Attitude:
9592
9693
:return: Attitude in SED frame
9794
"""
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
9996
r = Rotation.from_quat(self.q) * Rotation.from_quat(nadir_pitch)
10097
q = r.as_quat()
10198
q = np.array([q[1], -q[0], q[2], -q[3]]) # NED to ESD
@@ -163,40 +160,40 @@ def __post_init__(self):
163160
@dataclass(frozen=True)
164161
class MapData(_ImageHolder):
165162
"""Keeps map frame related data in one place and protects it from corruption."""
166-
#bbox: GeoSquare
167163
bbox: BBox
168164
elevation: Optional[Img] = None # Optional elevation raster
169165

170166
def __post_init__(self):
171167
"""Post-initialization validity check."""
172168
if self.elevation is not None:
173169
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
175171

176172

177173
# noinspection PyClassHasNoInit
178174
@dataclass(frozen=True)
179175
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
182178
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
186182
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)
189185

186+
# TODO: update docs - only one transformation is returned
190187
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
193190
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
195192
the original map.
196193
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.
200197
"""
201198
map_dim_arr = np.array(self.map_data.image.dim)
202199
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]
212209
uncropped_to_unrotated = np.vstack((rotation, rotation_padding))
213210

214211
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)
216212
coords = np.array(box(*self.map_data.bbox).exterior.coords)
217213
gt = GeoTrapezoid(coords) # .reshape(-1, 1, 2)
218-
#dst_corners = np.flip(coords[:-1], axis=1) # from ENU frame to WGS 84 axis order
219214
dst_corners = gt.square_coords
220215
dst_corners = np.flip(dst_corners, axis=1) # from ENU frame to WGS 84 axis order
221216
unrotated_to_wgs84 = cv2.getPerspectiveTransform(np.float32(src_corners).squeeze(),
222217
np.float32(dst_corners).squeeze())
223218

224219
# 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))
227220
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))
229222

230223
pix_to_wgs84_ = unrotated_to_wgs84 @ uncropped_to_unrotated @ pix_to_uncropped
231224

@@ -234,9 +227,7 @@ def _pix_to_wgs84(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
234227
return pix_to_wgs84_ # , unrotated_to_wgs84, uncropped_to_unrotated, pix_to_uncropped
235228

236229
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
240231
241232
:param elevation: Set True to do rotation on elevation raster instead
242233
:return: Rotated and cropped map raster
@@ -260,7 +251,7 @@ def _rotate_and_crop_map(self, elevation: bool = False) -> np.ndarray:
260251

261252
@staticmethod
262253
def _crop_center(img: np.ndarray, dimensions: Dim) -> np.ndarray:
263-
"""Crops dimensions sized part from center.
254+
"""Crops dimensions sized part from center
264255
265256
:param img: Image to crop
266257
: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:
269260
cx, cy = tuple(np.array(img.shape[0:2]) / 2)
270261
img_cropped = img[math.floor(cy - dimensions.height / 2):math.floor(cy + dimensions.height / 2),
271262
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.'
275265
return img_cropped
276266

277267
def __post_init__(self):
@@ -319,21 +309,6 @@ def __iter__(self):
319309
yield item
320310

321311

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-
337312
# noinspection PyClassHasNoInit
338313
@dataclass(frozen=True)
339314
class Altitude:
@@ -345,7 +320,7 @@ class Altitude:
345320
home: Above home or starting location
346321
347322
.. note::
348-
Altitude AGL should always be known (cannot be None)
323+
TODO: Altitude AGL should always be known (cannot be None)
349324
350325
.. seealso::
351326
`Altitude definitions <https://ardupilot.org/copter/docs/common-understanding-altitude.html>`_
@@ -355,24 +330,12 @@ class Altitude:
355330
ellipsoid: Optional[float]
356331
home: Optional[float]
357332

333+
# TODO
358334
#def __post_init__(self):
359335
# """Post-initialization validity checks"""
360336
# assert self.agl is not None
361337

362338

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-
376339
# noinspection PyClassHasNoInit
377340
@dataclass
378341
class FOV:
@@ -418,16 +381,16 @@ def __post_init__(self):
418381
class FixedCamera:
419382
"""WGS84-fixed camera attributes
420383
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.
423387
"""
424388
image_pair: ImagePair
425389
pose: Pose
426390
timestamp: int
427-
#snapshot: Snapshot
428391
terrain_altitude_amsl: Optional[float]
429392
terrain_altitude_ellipsoid: Optional[float]
430-
home_position: Optional[ROSGeoPoint]
393+
home_position: Optional[GeoPoint]
431394
fov: FOV = field(init=False)
432395
position: Position = field(init=False)
433396
h: np.ndarray = field(init=False)
@@ -459,7 +422,7 @@ def _estimate_fov(self) -> Optional[FOV]:
459422
# Could not create a valid FOV
460423
return None
461424

462-
def _estimate_attitude(self) -> np.ndarray:
425+
def _estimate_attitude(self) -> Attitude:
463426
"""Estimates gimbal (not vehicle) attitude in NED frame
464427
465428
.. note::
@@ -473,7 +436,6 @@ def _estimate_attitude(self) -> np.ndarray:
473436
assert not np.isnan(rT).any()
474437
gimbal_estimated_attitude = Rotation.from_matrix(rT) # rotated map pixel frame
475438

476-
#gimbal_estimated_attitude *= Rotation.from_rotvec(-(np.pi/2) * np.array([0, 1, 0])) # camera body
477439
gimbal_estimated_attitude *= Rotation.from_rotvec(
478440
self.image_pair.ref.rotation * np.array([0, 0, 1])) # unrotated map pixel frame
479441

@@ -489,7 +451,7 @@ def _get_fov_and_c(img_arr_shape: Tuple[int, int], h_mat: np.ndarray) -> Tuple[n
489451
490452
:param img_arr_shape: Image array shape tuple (height, width)
491453
: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
493455
"""
494456
assert_type(img_arr_shape, tuple)
495457
assert_len(img_arr_shape, 2)
@@ -519,13 +481,12 @@ def _estimate_position(self, terrain_altitude_amsl: Optional[float], terrain_alt
519481
:param terrain_altitude_amsl: Optional ground elevation above AMSL in meters
520482
:param terrain_altitude_ellipsoid: Optional ground elevation above WGS 84 ellipsoid in meters
521483
:param crs: CRS to use for the Position
522-
:return: Camera position
484+
:return: Camera position or None if not available
523485
"""
524486
assert self.fov is not None # Call _estimate_fov before _estimate_position!
525487
# Translation in WGS84 (and altitude or z-axis translation in meters above ground)
526488
assert_type(self.image_pair.ref, ContextualMapData) # need pix_to_wgs84
527489
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
529490
t_wgs84[2] = -self.fov.scaling * self.camera_position[2] # In NED frame z-coordinate is negative above ground, make altitude >0
530491

531492
# Check that we have all the values needed for a global position
@@ -560,9 +521,6 @@ def __post_init__(self):
560521
raise DataValueError('Please provide valid image pair.')
561522

562523
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.')
566524
if self.terrain_altitude_amsl is not None and self.terrain_altitude_ellipsoid is None or \
567525
self.terrain_altitude_amsl is not None and self.terrain_altitude_ellipsoid is None:
568526
raise DataValueError('Please provide terrain altitude in both AMSL and above WGS 84 ellipsoid.')

0 commit comments

Comments
 (0)