Skip to content

Commit 1213ebf

Browse files
authored
feat: support GPS timestamps (#695)
* remove _ecef_from_lla_DEPRECATED * move PointWithFix to telemetry.GPSPoint * rename GPS fields * format with latest ruff * add epoch time * add the missing telemetry.py * format * fix format again * refactor * fix tests
1 parent 1aa5b41 commit 1213ebf

25 files changed

+225
-210
lines changed

mapillary_tools/camm/camm_builder.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ def _create_edit_list(
4343
]
4444
break
4545

46-
assert (
47-
0 <= points[0].time
48-
), f"expect non-negative point time but got {points[0]}"
49-
assert (
50-
points[0].time <= points[-1].time
51-
), f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
46+
assert 0 <= points[0].time, (
47+
f"expect non-negative point time but got {points[0]}"
48+
)
49+
assert points[0].time <= points[-1].time, (
50+
f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
51+
)
5252

5353
if idx == 0:
5454
if 0 < points[0].time:
@@ -92,9 +92,9 @@ def convert_points_to_raw_samples(
9292
timedelta = int((points[idx + 1].time - point.time) * timescale)
9393
else:
9494
timedelta = 0
95-
assert (
96-
0 <= timedelta <= builder.UINT32_MAX
97-
), f"expected timedelta {timedelta} between {points[idx]} and {points[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
95+
assert 0 <= timedelta <= builder.UINT32_MAX, (
96+
f"expected timedelta {timedelta} between {points[idx]} and {points[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
97+
)
9898

9999
yield sample_parser.RawSample(
100100
# will update later

mapillary_tools/exiftool_read_video.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import xml.etree.ElementTree as ET
66

77
from . import exif_read, exiftool_read, geo
8+
from .telemetry import GPSFix, GPSPoint
89

910

1011
MAX_TRACK_ID = 10
@@ -87,7 +88,7 @@ def _aggregate_gps_track(
8788
alt_tag: T.Optional[str] = None,
8889
direction_tag: T.Optional[str] = None,
8990
ground_speed_tag: T.Optional[str] = None,
90-
) -> T.List[geo.PointWithFix]:
91+
) -> T.List[GPSPoint]:
9192
"""
9293
Aggregate all GPS data by the tags.
9394
It requires lat, lon to be present, and their lengths must match.
@@ -173,15 +174,16 @@ def _aggregate_float_values_same_length(
173174
if timestamp is None or lon is None or lat is None:
174175
continue
175176
track.append(
176-
geo.PointWithFix(
177+
GPSPoint(
177178
time=timestamp,
178179
lon=lon,
179180
lat=lat,
180181
alt=alt,
181182
angle=direction,
182-
gps_fix=None,
183-
gps_precision=None,
184-
gps_ground_speed=ground_speed,
183+
epoch_time=None,
184+
fix=None,
185+
precision=None,
186+
ground_speed=ground_speed,
185187
)
186188
)
187189

@@ -230,8 +232,8 @@ def _aggregate_gps_track_by_sample_time(
230232
ground_speed_tag: T.Optional[str] = None,
231233
gps_fix_tag: T.Optional[str] = None,
232234
gps_precision_tag: T.Optional[str] = None,
233-
) -> T.List[geo.PointWithFix]:
234-
track: T.List[geo.PointWithFix] = []
235+
) -> T.List[GPSPoint]:
236+
track: T.List[GPSPoint] = []
235237

236238
expanded_gps_fix_tag = None
237239
if gps_fix_tag is not None:
@@ -249,7 +251,7 @@ def _aggregate_gps_track_by_sample_time(
249251
gps_fix_texts = texts_by_tag.get(expanded_gps_fix_tag)
250252
if gps_fix_texts:
251253
try:
252-
gps_fix = geo.GPSFix(int(gps_fix_texts[0]))
254+
gps_fix = GPSFix(int(gps_fix_texts[0]))
253255
except ValueError:
254256
gps_fix = None
255257

@@ -280,7 +282,7 @@ def _aggregate_gps_track_by_sample_time(
280282
for idx, point in enumerate(points):
281283
point.time = sample_time + idx * avg_timedelta
282284
track.extend(
283-
dataclasses.replace(point, gps_fix=gps_fix, gps_precision=gps_precision)
285+
dataclasses.replace(point, fix=gps_fix, precision=gps_precision)
284286
for point in points
285287
)
286288

@@ -355,7 +357,7 @@ def extract_model(self) -> T.Optional[str]:
355357
_, model = self._extract_make_and_model()
356358
return model
357359

358-
def _extract_gps_track_from_track(self) -> T.List[geo.PointWithFix]:
360+
def _extract_gps_track_from_track(self) -> T.List[GPSPoint]:
359361
for track_id in range(1, MAX_TRACK_ID + 1):
360362
track_ns = f"Track{track_id}"
361363
if self._all_tags_exists(
@@ -397,7 +399,7 @@ def _all_tags_exists(self, tags: T.Set[str]) -> bool:
397399

398400
def _extract_gps_track_from_quicktime(
399401
self, namespace: str = "QuickTime"
400-
) -> T.List[geo.PointWithFix]:
402+
) -> T.List[GPSPoint]:
401403
if not self._all_tags_exists(
402404
{
403405
expand_tag(f"{namespace}:GPSDateTime"),

mapillary_tools/ffmpeg.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,10 @@ def generate_binary_search(self, sorted_frame_indices: T.Sequence[int]) -> str:
202202
return "0"
203203

204204
if length == 1:
205-
return f"eq(n\\,{ sorted_frame_indices[0] })"
205+
return f"eq(n\\,{sorted_frame_indices[0]})"
206206

207207
middle = length // 2
208-
return f"if(lt(n\\,{ sorted_frame_indices[middle] })\\,{ self.generate_binary_search(sorted_frame_indices[:middle]) }\\,{ self.generate_binary_search(sorted_frame_indices[middle:]) })"
208+
return f"if(lt(n\\,{sorted_frame_indices[middle]})\\,{self.generate_binary_search(sorted_frame_indices[:middle])}\\,{self.generate_binary_search(sorted_frame_indices[middle:])})"
209209

210210
def extract_specified_frames(
211211
self,

mapillary_tools/geo.py

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import itertools
77
import math
88
import typing as T
9-
from enum import Enum, unique
109

1110
WGS84_a = 6378137.0
1211
WGS84_a_SQ = WGS84_a**2
@@ -32,45 +31,6 @@ class Point:
3231
angle: T.Optional[float]
3332

3433

35-
@unique
36-
class GPSFix(Enum):
37-
NO_FIX = 0
38-
FIX_2D = 2
39-
FIX_3D = 3
40-
41-
42-
@dataclasses.dataclass
43-
class PointWithFix(Point):
44-
gps_fix: T.Optional[GPSFix]
45-
gps_precision: T.Optional[float]
46-
gps_ground_speed: T.Optional[float]
47-
48-
49-
def _ecef_from_lla_DEPRECATED(
50-
lat: float, lon: float, alt: float
51-
) -> T.Tuple[float, float, float]:
52-
"""
53-
Deprecated because it is slow. Keep here for reference and comparison.
54-
Use _ecef_from_lla2 instead.
55-
56-
Compute ECEF XYZ from latitude, longitude and altitude.
57-
58-
All using the WGS94 model.
59-
Altitude is the distance to the WGS94 ellipsoid.
60-
Check results here http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm
61-
62-
"""
63-
a2 = WGS84_a**2
64-
b2 = WGS84_b**2
65-
lat = math.radians(lat)
66-
lon = math.radians(lon)
67-
L = 1.0 / math.sqrt(a2 * math.cos(lat) ** 2 + b2 * math.sin(lat) ** 2)
68-
x = (a2 * L + alt) * math.cos(lat) * math.cos(lon)
69-
y = (a2 * L + alt) * math.cos(lat) * math.sin(lon)
70-
z = (b2 * L + alt) * math.sin(lat)
71-
return x, y, z
72-
73-
7434
def _ecef_from_lla2(lat: float, lon: float) -> T.Tuple[float, float, float]:
7535
"""
7636
Compute ECEF XYZ from latitude, longitude and altitude.
@@ -172,20 +132,6 @@ def pairwise(iterable: T.Iterable[_IT]) -> T.Iterable[T.Tuple[_IT, _IT]]:
172132
return zip(a, b)
173133

174134

175-
def group_every(
176-
iterable: T.Iterable[_IT], n: int
177-
) -> T.Generator[T.Generator[_IT, None, None], None, None]:
178-
"""
179-
Return a generator that divides the iterable into groups by N.
180-
"""
181-
182-
if not (0 < n):
183-
raise ValueError("expect 0 < n but got {0}".format(n))
184-
185-
for _, group in itertools.groupby(enumerate(iterable), key=lambda t: t[0] // n):
186-
yield (item for _, item in group)
187-
188-
189135
def as_unix_time(dt: T.Union[datetime.datetime, int, float]) -> float:
190136
if isinstance(dt, (int, float)):
191137
return dt

mapillary_tools/geotag/geotag_videos_from_exiftool_video.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from .. import exceptions, exiftool_read, geo, types
1010
from ..exiftool_read_video import ExifToolReadVideo
11+
from ..telemetry import GPSPoint
1112
from . import gpmf_gps_filter, utils as video_utils
1213
from .geotag_from_generic import GeotagVideosFromGeneric
1314

@@ -45,11 +46,11 @@ def geotag_video(element: ET.Element) -> types.VideoMetadataOrError:
4546
points = geo.extend_deduplicate_points(points)
4647
assert points, "must have at least one point"
4748

48-
if all(isinstance(p, geo.PointWithFix) for p in points):
49+
if all(isinstance(p, GPSPoint) for p in points):
4950
points = T.cast(
5051
T.List[geo.Point],
5152
gpmf_gps_filter.remove_noisy_points(
52-
T.cast(T.List[geo.PointWithFix], points)
53+
T.cast(T.List[GPSPoint], points)
5354
),
5455
)
5556
if not points:

mapillary_tools/geotag/geotag_videos_from_video.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .. import exceptions, geo, types
1010
from ..camm import camm_parser
1111
from ..mp4 import simple_mp4_parser as sparser
12+
from ..telemetry import GPSPoint
1213
from . import blackvue_parser, gpmf_gps_filter, gpmf_parser, utils as video_utils
1314
from .geotag_from_generic import GeotagVideosFromGeneric
1415

@@ -155,11 +156,11 @@ def geotag_video(
155156
video_metadata.points = geo.extend_deduplicate_points(video_metadata.points)
156157
assert video_metadata.points, "must have at least one point"
157158

158-
if all(isinstance(p, geo.PointWithFix) for p in video_metadata.points):
159+
if all(isinstance(p, GPSPoint) for p in video_metadata.points):
159160
video_metadata.points = T.cast(
160161
T.List[geo.Point],
161162
gpmf_gps_filter.remove_noisy_points(
162-
T.cast(T.List[geo.PointWithFix], video_metadata.points)
163+
T.cast(T.List[GPSPoint], video_metadata.points)
163164
),
164165
)
165166
if not video_metadata.points:

mapillary_tools/geotag/gpmf_gps_filter.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import typing as T
33

44
from .. import constants, geo
5+
from ..telemetry import GPSPoint
56
from . import gps_filter
67

78
"""
@@ -13,8 +14,8 @@
1314

1415

1516
def remove_outliers(
16-
sequence: T.Sequence[geo.PointWithFix],
17-
) -> T.Sequence[geo.PointWithFix]:
17+
sequence: T.Sequence[GPSPoint],
18+
) -> T.Sequence[GPSPoint]:
1819
distances = [
1920
geo.gps_distance((left.lat, left.lon), (right.lat, right.lon))
2021
for left, right in geo.pairwise(sequence)
@@ -37,9 +38,7 @@ def remove_outliers(
3738
"Split to %d sequences with max distance %f", len(sequences), max_distance
3839
)
3940

40-
ground_speeds = [
41-
p.gps_ground_speed for p in sequence if p.gps_ground_speed is not None
42-
]
41+
ground_speeds = [p.ground_speed for p in sequence if p.ground_speed is not None]
4342
if len(ground_speeds) < 2:
4443
return sequence
4544

@@ -50,20 +49,20 @@ def remove_outliers(
5049
)
5150

5251
return T.cast(
53-
T.List[geo.PointWithFix],
52+
T.List[GPSPoint],
5453
gps_filter.find_majority(merged.values()),
5554
)
5655

5756

5857
def remove_noisy_points(
59-
sequence: T.Sequence[geo.PointWithFix],
60-
) -> T.Sequence[geo.PointWithFix]:
58+
sequence: T.Sequence[GPSPoint],
59+
) -> T.Sequence[GPSPoint]:
6160
num_points = len(sequence)
6261
sequence = [
6362
p
6463
for p in sequence
6564
# include points **without** GPS fix
66-
if p.gps_fix is None or p.gps_fix.value in constants.GOPRO_GPS_FIXES
65+
if p.fix is None or p.fix.value in constants.GOPRO_GPS_FIXES
6766
]
6867
if len(sequence) < num_points:
6968
LOG.debug(
@@ -77,7 +76,7 @@ def remove_noisy_points(
7776
p
7877
for p in sequence
7978
# include points **without** precision
80-
if p.gps_precision is None or p.gps_precision <= constants.GOPRO_MAX_DOP100
79+
if p.precision is None or p.precision <= constants.GOPRO_MAX_DOP100
8180
]
8281
if len(sequence) < num_points:
8382
LOG.debug(

0 commit comments

Comments
 (0)