mapillary-tools 0.13.3__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.
Files changed (87) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +198 -55
  3. mapillary_tools/authenticate.py +326 -64
  4. mapillary_tools/blackvue_parser.py +195 -0
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +429 -181
  7. mapillary_tools/commands/__main__.py +10 -6
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +18 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +44 -13
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +65 -26
  15. mapillary_tools/constants.py +141 -18
  16. mapillary_tools/exceptions.py +37 -34
  17. mapillary_tools/exif_read.py +221 -116
  18. mapillary_tools/exif_write.py +10 -8
  19. mapillary_tools/exiftool_read.py +33 -42
  20. mapillary_tools/exiftool_read_video.py +97 -47
  21. mapillary_tools/exiftool_runner.py +57 -0
  22. mapillary_tools/ffmpeg.py +417 -242
  23. mapillary_tools/geo.py +158 -118
  24. mapillary_tools/geotag/__init__.py +0 -1
  25. mapillary_tools/geotag/base.py +147 -0
  26. mapillary_tools/geotag/factory.py +307 -0
  27. mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
  28. mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
  29. mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
  30. mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
  31. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
  32. mapillary_tools/geotag/geotag_images_from_video.py +88 -51
  33. mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
  34. mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
  35. mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
  36. mapillary_tools/geotag/image_extractors/base.py +18 -0
  37. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  38. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  39. mapillary_tools/geotag/options.py +182 -0
  40. mapillary_tools/geotag/utils.py +52 -16
  41. mapillary_tools/geotag/video_extractors/base.py +18 -0
  42. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  43. mapillary_tools/geotag/video_extractors/gpx.py +116 -0
  44. mapillary_tools/geotag/video_extractors/native.py +160 -0
  45. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
  46. mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
  47. mapillary_tools/history.py +134 -20
  48. mapillary_tools/mp4/construct_mp4_parser.py +17 -10
  49. mapillary_tools/mp4/io_utils.py +0 -1
  50. mapillary_tools/mp4/mp4_sample_parser.py +36 -28
  51. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  52. mapillary_tools/mp4/simple_mp4_parser.py +13 -22
  53. mapillary_tools/process_geotag_properties.py +184 -414
  54. mapillary_tools/process_sequence_properties.py +594 -225
  55. mapillary_tools/sample_video.py +20 -26
  56. mapillary_tools/serializer/description.py +587 -0
  57. mapillary_tools/serializer/gpx.py +132 -0
  58. mapillary_tools/telemetry.py +26 -13
  59. mapillary_tools/types.py +98 -611
  60. mapillary_tools/upload.py +411 -387
  61. mapillary_tools/upload_api_v4.py +167 -142
  62. mapillary_tools/uploader.py +804 -284
  63. mapillary_tools/utils.py +49 -18
  64. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
  65. mapillary_tools-0.14.0.dist-info/RECORD +75 -0
  66. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
  67. mapillary_tools/geotag/blackvue_parser.py +0 -118
  68. mapillary_tools/geotag/geotag_from_generic.py +0 -22
  69. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
  70. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
  71. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  72. mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
  73. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  74. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
  75. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
  76. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
  77. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
  78. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  79. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
  80. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
  81. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  82. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  83. mapillary_tools-0.13.3.dist-info/RECORD +0 -75
  84. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  85. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
  86. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
  87. {mapillary_tools-0.13.3.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: T.Optional[float]
31
- angle: T.Optional[float]
32
+ alt: float | None
33
+ angle: float | None
32
34
 
33
35
 
34
- def _ecef_from_lla2(lat: float, lon: float) -> T.Tuple[float, float, float]:
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 get_max_distance_from_start(latlons: T.List[T.Tuple[float, float]]) -> float:
73
- """
74
- Returns the radius of an entire GPS track. Used to calculate whether or not the entire sequence was just stationary video
75
- Takes a sequence of points as input
76
- """
77
- if not latlons:
78
- return 0
79
- start = latlons[0]
80
- return max(gps_distance(start, latlon) for latlon in latlons)
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
- start_lat: float, start_lon: float, end_lat: float, end_lon: float
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
- # make sure everything is in radians
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[T.Tuple[_IT, _IT]]:
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: T.Union[datetime.datetime, int, float]) -> float:
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
- def _interpolate_segment(start: Point, end: Point, t: float) -> Point:
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 _interpolate_at_index(points: T.Sequence[Point], t: float, idx: int):
170
- assert points, "expect non-empty points"
171
-
172
- # find the segment (start point, end point)
173
- if len(points) == 1:
174
- start, end = points[0], points[0]
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
- return _interpolate_segment(start, end, t)
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
- Interpolate or extrapolate the point at time t along the sequence of points (sorted by time).
193
- """
194
- if not points:
195
- raise ValueError("Expect non-empty points")
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
- # Make sure that points are sorted (disabled because the check costs O(N)):
198
- # for cur, nex in pairwise(points):
199
- # assert cur.time <= nex.time, "Points not sorted"
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
- p = Point(time=t, lat=float("-inf"), lon=float("-inf"), alt=None, angle=None)
202
- idx = bisect.bisect_left(points, p, lo=lo)
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: T.Optional[float]
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 tracks")
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: T.Optional[int] = None
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
- assert self.prev_time <= t, "requires time to be monotonically increasing"
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
- return _interpolate_at_index(track, t, 0)
238
+ interpolated = _interpolate_at_segment_idx(track, t, 0)
239
+ break
240
+
253
241
  elif track[0].time <= t <= track[-1].time:
254
- # similar to bisect.bisect_left(points, p, lo=lo) but faster in this case
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 sit between (track[idx - 1], track[idx]]
257
- # set the lower bound to idx - 1
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
- return _interpolate_at_index(track, t, idx)
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 = _interpolate_at_index(self.tracks[-1], t, len(self.tracks[-1]))
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
- _PointAbstract = T.TypeVar("_PointAbstract")
263
+ _T = T.TypeVar("_T")
272
264
 
273
265
 
274
266
  def sample_points_by_distance(
275
- samples: T.Iterable[_PointAbstract],
267
+ samples: T.Iterable[_T],
276
268
  min_distance: float,
277
- point_func: T.Callable[[_PointAbstract], Point],
278
- ) -> T.Generator[_PointAbstract, None, None]:
279
- prevp: T.Optional[Point] = None
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[Point]) -> None:
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
- sequence[-1].angle = sequence[-2].angle
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
- _PointLike = T.TypeVar("_PointLike", bound=Point)
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 extend_deduplicate_points(
308
- sequence: T.Iterable[_PointLike],
309
- to_extend: T.Optional[T.List[_PointLike]] = None,
310
- ) -> T.List[_PointLike]:
311
- if to_extend is None:
312
- to_extend = []
313
- for point in sequence:
314
- if to_extend:
315
- prev = to_extend[-1].lon, to_extend[-1].lat
316
- cur = (point.lon, point.lat)
317
- if cur != prev:
318
- to_extend.append(point)
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
- to_extend.append(point)
321
- return to_extend
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