mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +287 -22
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +17 -8
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +408 -416
- mapillary_tools/upload_api_v4.py +172 -174
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
mapillary_tools/geo.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# pyre-ignore-all-errors[4]
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import bisect
|
|
4
5
|
import dataclasses
|
|
5
6
|
import datetime
|
|
6
7
|
import itertools
|
|
7
8
|
import math
|
|
9
|
+
import sys
|
|
8
10
|
import typing as T
|
|
9
11
|
|
|
10
12
|
WGS84_a = 6378137.0
|
|
@@ -27,34 +29,14 @@ class Point:
|
|
|
27
29
|
time: float
|
|
28
30
|
lat: float
|
|
29
31
|
lon: float
|
|
30
|
-
alt:
|
|
31
|
-
angle:
|
|
32
|
+
alt: float | None
|
|
33
|
+
angle: float | None
|
|
32
34
|
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
"""
|
|
36
|
-
Compute ECEF XYZ from latitude, longitude and altitude.
|
|
37
|
-
|
|
38
|
-
All using the WGS94 model.
|
|
39
|
-
Altitude is the distance to the WGS94 ellipsoid.
|
|
40
|
-
Check results here http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm
|
|
41
|
-
|
|
42
|
-
"""
|
|
43
|
-
lat = math.radians(lat)
|
|
44
|
-
lon = math.radians(lon)
|
|
45
|
-
cos_lat = math.cos(lat)
|
|
46
|
-
sin_lat = math.sin(lat)
|
|
47
|
-
L = 1.0 / math.sqrt(WGS84_a_SQ * cos_lat**2 + WGS84_b_SQ * sin_lat**2)
|
|
48
|
-
K = WGS84_a_SQ * L * cos_lat
|
|
49
|
-
x = K * math.cos(lon)
|
|
50
|
-
y = K * math.sin(lon)
|
|
51
|
-
z = WGS84_b_SQ * L * sin_lat
|
|
52
|
-
return x, y, z
|
|
36
|
+
PointLike = T.TypeVar("PointLike", bound=Point)
|
|
53
37
|
|
|
54
38
|
|
|
55
|
-
def gps_distance(
|
|
56
|
-
latlon_1: T.Tuple[float, float], latlon_2: T.Tuple[float, float]
|
|
57
|
-
) -> float:
|
|
39
|
+
def gps_distance(latlon_1: tuple[float, float], latlon_2: tuple[float, float]) -> float:
|
|
58
40
|
"""
|
|
59
41
|
Distance between two (lat,lon) pairs.
|
|
60
42
|
|
|
@@ -69,19 +51,25 @@ def gps_distance(
|
|
|
69
51
|
return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)
|
|
70
52
|
|
|
71
53
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
54
|
+
def avg_speed(sequence: T.Sequence[PointLike]) -> float:
|
|
55
|
+
total_distance = 0.0
|
|
56
|
+
for cur, nxt in pairwise(sequence):
|
|
57
|
+
total_distance += gps_distance((cur.lat, cur.lon), (nxt.lat, nxt.lon))
|
|
58
|
+
|
|
59
|
+
if sequence:
|
|
60
|
+
time_diff = sequence[-1].time - sequence[0].time
|
|
61
|
+
else:
|
|
62
|
+
time_diff = 0.0
|
|
63
|
+
|
|
64
|
+
if time_diff == 0.0:
|
|
65
|
+
return float("inf")
|
|
66
|
+
|
|
67
|
+
return total_distance / time_diff
|
|
81
68
|
|
|
82
69
|
|
|
83
70
|
def compute_bearing(
|
|
84
|
-
|
|
71
|
+
latlon_1: tuple[float, float],
|
|
72
|
+
latlon_2: tuple[float, float],
|
|
85
73
|
) -> float:
|
|
86
74
|
"""
|
|
87
75
|
Get the compass bearing from start to end.
|
|
@@ -89,7 +77,10 @@ def compute_bearing(
|
|
|
89
77
|
Formula from
|
|
90
78
|
http://www.movable-type.co.uk/scripts/latlong.html
|
|
91
79
|
"""
|
|
92
|
-
|
|
80
|
+
start_lat, start_lon = latlon_1
|
|
81
|
+
end_lat, end_lon = latlon_2
|
|
82
|
+
|
|
83
|
+
# Make sure everything is in radians
|
|
93
84
|
start_lat = math.radians(start_lat)
|
|
94
85
|
start_lon = math.radians(start_lon)
|
|
95
86
|
end_lat = math.radians(end_lat)
|
|
@@ -125,14 +116,14 @@ _IT = T.TypeVar("_IT")
|
|
|
125
116
|
|
|
126
117
|
|
|
127
118
|
# http://stackoverflow.com/a/5434936
|
|
128
|
-
def pairwise(iterable: T.Iterable[_IT]) -> T.Iterable[
|
|
119
|
+
def pairwise(iterable: T.Iterable[_IT]) -> T.Iterable[tuple[_IT, _IT]]:
|
|
129
120
|
"""s -> (s0,s1), (s1,s2), (s2, s3), ..."""
|
|
130
121
|
a, b = itertools.tee(iterable)
|
|
131
122
|
next(b, None)
|
|
132
123
|
return zip(a, b)
|
|
133
124
|
|
|
134
125
|
|
|
135
|
-
def as_unix_time(dt:
|
|
126
|
+
def as_unix_time(dt: datetime.datetime | int | float) -> float:
|
|
136
127
|
if isinstance(dt, (int, float)):
|
|
137
128
|
return dt
|
|
138
129
|
else:
|
|
@@ -148,59 +139,37 @@ def as_unix_time(dt: T.Union[datetime.datetime, int, float]) -> float:
|
|
|
148
139
|
return 0.0
|
|
149
140
|
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
if start.time == end.time:
|
|
153
|
-
weight = 0.0
|
|
154
|
-
else:
|
|
155
|
-
weight = (t - start.time) / (end.time - start.time)
|
|
156
|
-
|
|
157
|
-
lat = start.lat + (end.lat - start.lat) * weight
|
|
158
|
-
lon = start.lon + (end.lon - start.lon) * weight
|
|
159
|
-
angle = compute_bearing(start.lat, start.lon, end.lat, end.lon)
|
|
160
|
-
alt: T.Optional[float]
|
|
161
|
-
if start.alt is not None and end.alt is not None:
|
|
162
|
-
alt = start.alt + (end.alt - start.alt) * weight
|
|
163
|
-
else:
|
|
164
|
-
alt = None
|
|
165
|
-
|
|
166
|
-
return Point(time=t, lat=lat, lon=lon, alt=alt, angle=angle)
|
|
167
|
-
|
|
142
|
+
if sys.version_info < (3, 10):
|
|
168
143
|
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
else:
|
|
176
|
-
if 0 < idx < len(points):
|
|
177
|
-
# interpolating within the range
|
|
178
|
-
start, end = points[idx - 1], points[idx]
|
|
179
|
-
elif idx <= 0:
|
|
180
|
-
# extrapolating behind the range
|
|
181
|
-
start, end = points[0], points[1]
|
|
182
|
-
else:
|
|
183
|
-
# extrapolating beyond the range
|
|
184
|
-
assert len(points) <= idx
|
|
185
|
-
start, end = points[-2], points[-1]
|
|
144
|
+
def interpolate(points: T.Sequence[Point], t: float, lo: int = 0) -> Point:
|
|
145
|
+
"""
|
|
146
|
+
Interpolate or extrapolate the point at time t along the sequence of points (sorted by time).
|
|
147
|
+
"""
|
|
148
|
+
if not points:
|
|
149
|
+
raise ValueError("Expect non-empty points")
|
|
186
150
|
|
|
187
|
-
|
|
151
|
+
# Make sure that points are sorted (disabled because the check costs O(N)):
|
|
152
|
+
# for cur, nex in pairwise(points):
|
|
153
|
+
# assert cur.time <= nex.time, "Points not sorted"
|
|
188
154
|
|
|
155
|
+
p = Point(time=t, lat=float("-inf"), lon=float("-inf"), alt=None, angle=None)
|
|
156
|
+
idx = bisect.bisect_left(points, p, lo=lo)
|
|
157
|
+
return _interpolate_at_segment_idx(points, t, idx)
|
|
158
|
+
else:
|
|
189
159
|
|
|
190
|
-
def interpolate(points: T.Sequence[Point], t: float, lo: int = 0) -> Point:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
160
|
+
def interpolate(points: T.Sequence[Point], t: float, lo: int = 0) -> Point:
|
|
161
|
+
"""
|
|
162
|
+
Interpolate or extrapolate the point at time t along the sequence of points (sorted by time).
|
|
163
|
+
"""
|
|
164
|
+
if not points:
|
|
165
|
+
raise ValueError("Expect non-empty points")
|
|
196
166
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
167
|
+
# Make sure that points are sorted (disabled because the check costs O(N)):
|
|
168
|
+
# for cur, nex in pairwise(points):
|
|
169
|
+
# assert cur.time <= nex.time, "Points not sorted"
|
|
200
170
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return _interpolate_at_index(points, t, idx)
|
|
171
|
+
idx = bisect.bisect_left(points, t, lo=lo, key=lambda x: x.time)
|
|
172
|
+
return _interpolate_at_segment_idx(points, t, idx)
|
|
204
173
|
|
|
205
174
|
|
|
206
175
|
class Interpolator:
|
|
@@ -212,12 +181,22 @@ class Interpolator:
|
|
|
212
181
|
track_idx: int
|
|
213
182
|
# interpolation starts from the lower bound point index in the current track
|
|
214
183
|
lo: int
|
|
215
|
-
prev_time:
|
|
184
|
+
prev_time: float | None
|
|
216
185
|
|
|
217
186
|
def __init__(self, tracks: T.Sequence[T.Sequence[Point]]):
|
|
187
|
+
# Remove empty tracks
|
|
218
188
|
self.tracks = [track for track in tracks if track]
|
|
189
|
+
|
|
219
190
|
if not self.tracks:
|
|
220
|
-
raise ValueError("Expect non-empty
|
|
191
|
+
raise ValueError("Expect at least one non-empty track")
|
|
192
|
+
|
|
193
|
+
for track in self.tracks:
|
|
194
|
+
for left, right in pairwise(track):
|
|
195
|
+
if not (left.time <= right.time):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"Expect points to be sorted by time, but got {left.time} then {right.time}"
|
|
198
|
+
)
|
|
199
|
+
|
|
221
200
|
self.tracks.sort(key=lambda track: track[0].time)
|
|
222
201
|
self.track_idx = 0
|
|
223
202
|
self.lo = 0
|
|
@@ -225,7 +204,7 @@ class Interpolator:
|
|
|
225
204
|
|
|
226
205
|
@staticmethod
|
|
227
206
|
def _lsearch_left(
|
|
228
|
-
track: T.Sequence[Point], t: float, lo: int = 0, hi:
|
|
207
|
+
track: T.Sequence[Point], t: float, lo: int = 0, hi: int | None = None
|
|
229
208
|
) -> int:
|
|
230
209
|
"""
|
|
231
210
|
similar to bisect.bisect_left, but faster in the incremental search case
|
|
@@ -244,39 +223,52 @@ class Interpolator:
|
|
|
244
223
|
|
|
245
224
|
def interpolate(self, t: float) -> Point:
|
|
246
225
|
if self.prev_time is not None:
|
|
247
|
-
|
|
226
|
+
if not (self.prev_time <= t):
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"Require times to be monotonically increasing, but got {self.prev_time} then {t}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
interpolated: Point | None = None
|
|
248
232
|
|
|
249
233
|
while self.track_idx < len(self.tracks):
|
|
250
234
|
track = self.tracks[self.track_idx]
|
|
235
|
+
assert track, "expect non-empty track"
|
|
236
|
+
|
|
251
237
|
if t < track[0].time:
|
|
252
|
-
|
|
238
|
+
interpolated = _interpolate_at_segment_idx(track, t, 0)
|
|
239
|
+
break
|
|
240
|
+
|
|
253
241
|
elif track[0].time <= t <= track[-1].time:
|
|
254
|
-
#
|
|
242
|
+
# Similar to bisect.bisect_left(points, p, lo=lo) but faster in this case
|
|
255
243
|
idx = Interpolator._lsearch_left(track, t, lo=self.lo)
|
|
256
|
-
# t must
|
|
257
|
-
#
|
|
258
|
-
# because the next t can still be interpolated anywhere between (track[idx - 1], track[idx]]
|
|
244
|
+
# Time t must be between (track[idx - 1], track[idx]], so set the lower bound to idx - 1
|
|
245
|
+
# Because the next t can still be interpolated anywhere between (track[idx - 1], track[idx]]
|
|
259
246
|
self.lo = max(idx - 1, 0)
|
|
260
|
-
|
|
247
|
+
interpolated = _interpolate_at_segment_idx(track, t, idx)
|
|
248
|
+
break
|
|
249
|
+
|
|
261
250
|
self.track_idx += 1
|
|
262
251
|
self.lo = 0
|
|
263
252
|
|
|
264
|
-
interpolated
|
|
253
|
+
if interpolated is None:
|
|
254
|
+
interpolated = _interpolate_at_segment_idx(
|
|
255
|
+
self.tracks[-1], t, len(self.tracks[-1])
|
|
256
|
+
)
|
|
265
257
|
|
|
266
258
|
self.prev_time = t
|
|
267
259
|
|
|
268
260
|
return interpolated
|
|
269
261
|
|
|
270
262
|
|
|
271
|
-
|
|
263
|
+
_T = T.TypeVar("_T")
|
|
272
264
|
|
|
273
265
|
|
|
274
266
|
def sample_points_by_distance(
|
|
275
|
-
samples: T.Iterable[
|
|
267
|
+
samples: T.Iterable[_T],
|
|
276
268
|
min_distance: float,
|
|
277
|
-
point_func: T.Callable[[
|
|
278
|
-
) -> T.Generator[
|
|
279
|
-
prevp:
|
|
269
|
+
point_func: T.Callable[[_T], Point],
|
|
270
|
+
) -> T.Generator[_T, None, None]:
|
|
271
|
+
prevp: Point | None = None
|
|
280
272
|
for sample in samples:
|
|
281
273
|
if prevp is None:
|
|
282
274
|
yield sample
|
|
@@ -288,34 +280,82 @@ def sample_points_by_distance(
|
|
|
288
280
|
prevp = p
|
|
289
281
|
|
|
290
282
|
|
|
291
|
-
def interpolate_directions_if_none(sequence: T.Sequence[
|
|
283
|
+
def interpolate_directions_if_none(sequence: T.Sequence[PointLike]) -> None:
|
|
292
284
|
for cur, nex in pairwise(sequence):
|
|
293
285
|
if cur.angle is None:
|
|
294
|
-
cur.angle = compute_bearing(cur.lat, cur.lon, nex.lat, nex.lon)
|
|
286
|
+
cur.angle = compute_bearing((cur.lat, cur.lon), (nex.lat, nex.lon))
|
|
295
287
|
|
|
296
288
|
if len(sequence) == 1:
|
|
297
289
|
if sequence[-1].angle is None:
|
|
298
290
|
sequence[-1].angle = 0
|
|
299
291
|
elif 2 <= len(sequence):
|
|
300
292
|
if sequence[-1].angle is None:
|
|
301
|
-
|
|
293
|
+
prev_angle = sequence[-2].angle
|
|
294
|
+
assert prev_angle is not None, (
|
|
295
|
+
"expect the last second point to have an interpolated angle"
|
|
296
|
+
)
|
|
297
|
+
sequence[-1].angle = prev_angle
|
|
298
|
+
|
|
302
299
|
|
|
300
|
+
def _ecef_from_lla2(lat: float, lon: float) -> tuple[float, float, float]:
|
|
301
|
+
"""
|
|
302
|
+
Compute ECEF XYZ from latitude and longitude.
|
|
303
|
+
|
|
304
|
+
All using the WGS94 model.
|
|
305
|
+
Altitude is the distance to the WGS94 ellipsoid.
|
|
306
|
+
Check results here http://www.oc.nps.edu/oc2902w/coord/llhxyz.htm
|
|
303
307
|
|
|
304
|
-
|
|
308
|
+
"""
|
|
309
|
+
lat = math.radians(lat)
|
|
310
|
+
lon = math.radians(lon)
|
|
311
|
+
cos_lat = math.cos(lat)
|
|
312
|
+
sin_lat = math.sin(lat)
|
|
313
|
+
L = 1.0 / math.sqrt(WGS84_a_SQ * cos_lat**2 + WGS84_b_SQ * sin_lat**2)
|
|
314
|
+
K = WGS84_a_SQ * L * cos_lat
|
|
315
|
+
x = K * math.cos(lon)
|
|
316
|
+
y = K * math.sin(lon)
|
|
317
|
+
z = WGS84_b_SQ * L * sin_lat
|
|
318
|
+
return x, y, z
|
|
305
319
|
|
|
306
320
|
|
|
307
|
-
def
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
def _interpolate_segment(start: Point, end: Point, t: float) -> Point:
|
|
322
|
+
try:
|
|
323
|
+
weight = (t - start.time) / (end.time - start.time)
|
|
324
|
+
except ZeroDivisionError:
|
|
325
|
+
weight = 0.0
|
|
326
|
+
|
|
327
|
+
lat = start.lat + (end.lat - start.lat) * weight
|
|
328
|
+
lon = start.lon + (end.lon - start.lon) * weight
|
|
329
|
+
angle = compute_bearing((start.lat, start.lon), (end.lat, end.lon))
|
|
330
|
+
alt: float | None
|
|
331
|
+
if start.alt is not None and end.alt is not None:
|
|
332
|
+
alt = start.alt + (end.alt - start.alt) * weight
|
|
333
|
+
else:
|
|
334
|
+
alt = None
|
|
335
|
+
|
|
336
|
+
return Point(time=t, lat=lat, lon=lon, alt=alt, angle=angle)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _interpolate_at_segment_idx(points: T.Sequence[Point], t: float, idx: int) -> Point:
|
|
340
|
+
"""
|
|
341
|
+
Interpolate time t along the segment between idx - 1 and idx.
|
|
342
|
+
If idx is out of range, extrapolate it to the nearest segment (first or last).
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
if len(points) == 1:
|
|
346
|
+
start, end = points[0], points[0]
|
|
347
|
+
elif 2 <= len(points):
|
|
348
|
+
if 0 < idx < len(points):
|
|
349
|
+
# Normal interpolation within the range
|
|
350
|
+
start, end = points[idx - 1], points[idx]
|
|
351
|
+
elif idx <= 0:
|
|
352
|
+
# Extrapolating before the first point
|
|
353
|
+
start, end = points[0], points[1]
|
|
319
354
|
else:
|
|
320
|
-
|
|
321
|
-
|
|
355
|
+
# Extrapolating after the last point
|
|
356
|
+
assert len(points) <= idx
|
|
357
|
+
start, end = points[-2], points[-1]
|
|
358
|
+
else:
|
|
359
|
+
assert False, "expect non-empty points"
|
|
360
|
+
|
|
361
|
+
return _interpolate_segment(start, end, t)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .. import geo # noqa: F401
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import logging
|
|
5
|
+
import typing as T
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
|
|
10
|
+
from .. import exceptions, types, utils
|
|
11
|
+
from .image_extractors.base import BaseImageExtractor
|
|
12
|
+
from .video_extractors.base import BaseVideoExtractor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOG = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
TImageExtractor = T.TypeVar("TImageExtractor", bound=BaseImageExtractor)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
22
|
+
"""
|
|
23
|
+
Extracts metadata from a list of image files with multiprocessing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, num_processes: int | None = None) -> None:
|
|
27
|
+
self.num_processes = num_processes
|
|
28
|
+
|
|
29
|
+
def to_description(
|
|
30
|
+
self, image_paths: T.Sequence[Path]
|
|
31
|
+
) -> list[types.ImageMetadataOrError]:
|
|
32
|
+
extractor_or_errors = self._generate_image_extractors(image_paths)
|
|
33
|
+
|
|
34
|
+
assert len(extractor_or_errors) == len(image_paths)
|
|
35
|
+
|
|
36
|
+
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
37
|
+
|
|
38
|
+
map_results = utils.mp_map_maybe(
|
|
39
|
+
self.run_extraction,
|
|
40
|
+
extractors,
|
|
41
|
+
num_processes=self.num_processes,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
results = list(
|
|
45
|
+
tqdm(
|
|
46
|
+
map_results,
|
|
47
|
+
desc="Extracting images",
|
|
48
|
+
unit="images",
|
|
49
|
+
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
50
|
+
total=len(extractors),
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return T.cast(list[types.ImageMetadataOrError], results + error_metadatas)
|
|
55
|
+
|
|
56
|
+
# This method is passed to multiprocessing
|
|
57
|
+
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
58
|
+
@classmethod
|
|
59
|
+
def run_extraction(cls, extractor: TImageExtractor) -> types.ImageMetadataOrError:
|
|
60
|
+
image_path = extractor.image_path
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return extractor.extract()
|
|
64
|
+
except exceptions.MapillaryDescriptionError as ex:
|
|
65
|
+
return types.describe_error_metadata(
|
|
66
|
+
ex, image_path, filetype=types.FileType.IMAGE
|
|
67
|
+
)
|
|
68
|
+
except exceptions.MapillaryUserError as ex:
|
|
69
|
+
# Considered as fatal error if not MapillaryDescriptionError
|
|
70
|
+
raise ex
|
|
71
|
+
except Exception as ex:
|
|
72
|
+
# TODO: hide details if not verbose mode
|
|
73
|
+
LOG.exception("Unexpected error extracting metadata from %s", image_path)
|
|
74
|
+
return types.describe_error_metadata(
|
|
75
|
+
ex, image_path, filetype=types.FileType.IMAGE
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _generate_image_extractors(
|
|
79
|
+
self, image_paths: T.Sequence[Path]
|
|
80
|
+
) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
TVideoExtractor = T.TypeVar("TVideoExtractor", bound=BaseVideoExtractor)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
88
|
+
"""
|
|
89
|
+
Extracts metadata from a list of video files with multiprocessing.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, num_processes: int | None = None) -> None:
|
|
93
|
+
self.num_processes = num_processes
|
|
94
|
+
|
|
95
|
+
def to_description(
|
|
96
|
+
self, video_paths: T.Sequence[Path]
|
|
97
|
+
) -> list[types.VideoMetadataOrError]:
|
|
98
|
+
extractor_or_errors = self._generate_video_extractors(video_paths)
|
|
99
|
+
|
|
100
|
+
assert len(extractor_or_errors) == len(video_paths)
|
|
101
|
+
|
|
102
|
+
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
103
|
+
|
|
104
|
+
map_results = utils.mp_map_maybe(
|
|
105
|
+
self.run_extraction,
|
|
106
|
+
extractors,
|
|
107
|
+
num_processes=self.num_processes,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
results = list(
|
|
111
|
+
tqdm(
|
|
112
|
+
map_results,
|
|
113
|
+
desc="Extracting videos",
|
|
114
|
+
unit="videos",
|
|
115
|
+
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
116
|
+
total=len(extractors),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
|
|
121
|
+
|
|
122
|
+
# This method is passed to multiprocessing
|
|
123
|
+
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
124
|
+
@classmethod
|
|
125
|
+
def run_extraction(cls, extractor: TVideoExtractor) -> types.VideoMetadataOrError:
|
|
126
|
+
video_path = extractor.video_path
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
return extractor.extract()
|
|
130
|
+
except exceptions.MapillaryDescriptionError as ex:
|
|
131
|
+
return types.describe_error_metadata(
|
|
132
|
+
ex, video_path, filetype=types.FileType.VIDEO
|
|
133
|
+
)
|
|
134
|
+
except exceptions.MapillaryUserError as ex:
|
|
135
|
+
# Considered as fatal error if not MapillaryDescriptionError
|
|
136
|
+
raise ex
|
|
137
|
+
except Exception as ex:
|
|
138
|
+
# TODO: hide details if not verbose mode
|
|
139
|
+
LOG.exception("Unexpected error extracting metadata from %s", video_path)
|
|
140
|
+
return types.describe_error_metadata(
|
|
141
|
+
ex, video_path, filetype=types.FileType.VIDEO
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _generate_video_extractors(
|
|
145
|
+
self, video_paths: T.Sequence[Path]
|
|
146
|
+
) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
|
|
147
|
+
raise NotImplementedError
|