mapillary-tools 0.14.0a1__py3-none-any.whl → 0.14.0b1__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 (76) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +5 -4
  3. mapillary_tools/authenticate.py +9 -9
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/camm/camm_parser.py +5 -5
  6. mapillary_tools/commands/__main__.py +1 -2
  7. mapillary_tools/config.py +41 -18
  8. mapillary_tools/constants.py +3 -2
  9. mapillary_tools/exceptions.py +1 -1
  10. mapillary_tools/exif_read.py +65 -65
  11. mapillary_tools/exif_write.py +7 -7
  12. mapillary_tools/exiftool_read.py +23 -46
  13. mapillary_tools/exiftool_read_video.py +88 -49
  14. mapillary_tools/exiftool_runner.py +4 -24
  15. mapillary_tools/ffmpeg.py +417 -242
  16. mapillary_tools/geo.py +4 -21
  17. mapillary_tools/geotag/__init__.py +0 -1
  18. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
  19. mapillary_tools/geotag/factory.py +105 -103
  20. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  21. mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
  22. mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
  23. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  24. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  25. mapillary_tools/geotag/geotag_images_from_video.py +51 -14
  26. mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
  27. mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
  28. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  29. mapillary_tools/geotag/image_extractors/base.py +18 -0
  30. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  31. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  32. mapillary_tools/geotag/options.py +26 -3
  33. mapillary_tools/geotag/utils.py +62 -0
  34. mapillary_tools/geotag/video_extractors/base.py +18 -0
  35. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  36. mapillary_tools/geotag/video_extractors/gpx.py +116 -0
  37. mapillary_tools/geotag/video_extractors/native.py +135 -0
  38. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  39. mapillary_tools/gpmf/gps_filter.py +5 -3
  40. mapillary_tools/history.py +8 -3
  41. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  42. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  43. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  44. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  45. mapillary_tools/process_geotag_properties.py +21 -15
  46. mapillary_tools/process_sequence_properties.py +49 -49
  47. mapillary_tools/sample_video.py +15 -14
  48. mapillary_tools/serializer/description.py +587 -0
  49. mapillary_tools/serializer/gpx.py +132 -0
  50. mapillary_tools/telemetry.py +6 -5
  51. mapillary_tools/types.py +64 -635
  52. mapillary_tools/upload.py +176 -197
  53. mapillary_tools/upload_api_v4.py +94 -51
  54. mapillary_tools/uploader.py +284 -138
  55. mapillary_tools/utils.py +16 -18
  56. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
  57. mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
  58. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
  59. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  60. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  61. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  62. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  63. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  64. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  65. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  66. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  67. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  68. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  69. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  70. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
  71. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  72. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  73. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  74. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
  75. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
  76. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
mapillary_tools/geo.py CHANGED
@@ -244,14 +244,14 @@ class Interpolator:
244
244
  return interpolated
245
245
 
246
246
 
247
- _PointAbstract = T.TypeVar("_PointAbstract")
247
+ _T = T.TypeVar("_T")
248
248
 
249
249
 
250
250
  def sample_points_by_distance(
251
- samples: T.Iterable[_PointAbstract],
251
+ samples: T.Iterable[_T],
252
252
  min_distance: float,
253
- point_func: T.Callable[[_PointAbstract], Point],
254
- ) -> T.Generator[_PointAbstract, None, None]:
253
+ point_func: T.Callable[[_T], Point],
254
+ ) -> T.Generator[_T, None, None]:
255
255
  prevp: Point | None = None
256
256
  for sample in samples:
257
257
  if prevp is None:
@@ -281,23 +281,6 @@ def interpolate_directions_if_none(sequence: T.Sequence[PointLike]) -> None:
281
281
  sequence[-1].angle = prev_angle
282
282
 
283
283
 
284
- def extend_deduplicate_points(
285
- sequence: T.Iterable[PointLike],
286
- to_extend: list[PointLike] | None = None,
287
- ) -> list[PointLike]:
288
- if to_extend is None:
289
- to_extend = []
290
- for point in sequence:
291
- if to_extend:
292
- prev = to_extend[-1].lon, to_extend[-1].lat
293
- cur = (point.lon, point.lat)
294
- if cur != prev:
295
- to_extend.append(point)
296
- else:
297
- to_extend.append(point)
298
- return to_extend
299
-
300
-
301
284
  def _ecef_from_lla2(lat: float, lon: float) -> tuple[float, float, float]:
302
285
  """
303
286
  Compute ECEF XYZ from latitude and longitude.
@@ -1 +0,0 @@
1
- from .. import geo # noqa: F401
@@ -8,24 +8,14 @@ from pathlib import Path
8
8
  from tqdm import tqdm
9
9
 
10
10
  from .. import exceptions, types, utils
11
+ from .image_extractors.base import BaseImageExtractor
12
+ from .video_extractors.base import BaseVideoExtractor
11
13
 
12
14
 
13
15
  LOG = logging.getLogger(__name__)
14
16
 
15
17
 
16
- class GenericImageExtractor(abc.ABC):
17
- """
18
- Extracts metadata from an image file.
19
- """
20
-
21
- def __init__(self, image_path: Path):
22
- self.image_path = image_path
23
-
24
- def extract(self) -> types.ImageMetadataOrError:
25
- raise NotImplementedError
26
-
27
-
28
- TImageExtractor = T.TypeVar("TImageExtractor", bound=GenericImageExtractor)
18
+ TImageExtractor = T.TypeVar("TImageExtractor", bound=BaseImageExtractor)
29
19
 
30
20
 
31
21
  class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
@@ -33,16 +23,15 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
33
23
  Extracts metadata from a list of image files with multiprocessing.
34
24
  """
35
25
 
36
- def __init__(
37
- self, image_paths: T.Sequence[Path], num_processes: int | None = None
38
- ) -> None:
39
- self.image_paths = image_paths
26
+ def __init__(self, num_processes: int | None = None) -> None:
40
27
  self.num_processes = num_processes
41
28
 
42
- def to_description(self) -> list[types.ImageMetadataOrError]:
43
- extractor_or_errors = self._generate_image_extractors()
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)
44
33
 
45
- assert len(extractor_or_errors) == len(self.image_paths)
34
+ assert len(extractor_or_errors) == len(image_paths)
46
35
 
47
36
  extractors, error_metadatas = types.separate_errors(extractor_or_errors)
48
37
 
@@ -62,12 +51,7 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
62
51
  )
63
52
  )
64
53
 
65
- return results + error_metadatas
66
-
67
- def _generate_image_extractors(
68
- self,
69
- ) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
70
- raise NotImplementedError
54
+ return T.cast(list[types.ImageMetadataOrError], results + error_metadatas)
71
55
 
72
56
  # This method is passed to multiprocessing
73
57
  # so it has to be classmethod or staticmethod to avoid pickling the instance
@@ -81,26 +65,23 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
81
65
  return types.describe_error_metadata(
82
66
  ex, image_path, filetype=types.FileType.IMAGE
83
67
  )
68
+ except exceptions.MapillaryUserError as ex:
69
+ # Considered as fatal error if not MapillaryDescriptionError
70
+ raise ex
84
71
  except Exception as ex:
72
+ # TODO: hide details if not verbose mode
85
73
  LOG.exception("Unexpected error extracting metadata from %s", image_path)
86
74
  return types.describe_error_metadata(
87
75
  ex, image_path, filetype=types.FileType.IMAGE
88
76
  )
89
77
 
90
-
91
- class GenericVideoExtractor(abc.ABC):
92
- """
93
- Extracts metadata from a video file.
94
- """
95
-
96
- def __init__(self, video_path: Path):
97
- self.video_path = video_path
98
-
99
- def extract(self) -> types.VideoMetadataOrError:
78
+ def _generate_image_extractors(
79
+ self, image_paths: T.Sequence[Path]
80
+ ) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
100
81
  raise NotImplementedError
101
82
 
102
83
 
103
- TVideoExtractor = T.TypeVar("TVideoExtractor", bound=GenericVideoExtractor)
84
+ TVideoExtractor = T.TypeVar("TVideoExtractor", bound=BaseVideoExtractor)
104
85
 
105
86
 
106
87
  class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
@@ -108,16 +89,15 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
108
89
  Extracts metadata from a list of video files with multiprocessing.
109
90
  """
110
91
 
111
- def __init__(
112
- self, video_paths: T.Sequence[Path], num_processes: int | None = None
113
- ) -> None:
114
- self.video_paths = video_paths
92
+ def __init__(self, num_processes: int | None = None) -> None:
115
93
  self.num_processes = num_processes
116
94
 
117
- def to_description(self) -> list[types.VideoMetadataOrError]:
118
- extractor_or_errors = self._generate_video_extractors()
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)
119
99
 
120
- assert len(extractor_or_errors) == len(self.video_paths)
100
+ assert len(extractor_or_errors) == len(video_paths)
121
101
 
122
102
  extractors, error_metadatas = types.separate_errors(extractor_or_errors)
123
103
 
@@ -137,12 +117,7 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
137
117
  )
138
118
  )
139
119
 
140
- return results + error_metadatas
141
-
142
- def _generate_video_extractors(
143
- self,
144
- ) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
145
- raise NotImplementedError
120
+ return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
146
121
 
147
122
  # This method is passed to multiprocessing
148
123
  # so it has to be classmethod or staticmethod to avoid pickling the instance
@@ -156,8 +131,17 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
156
131
  return types.describe_error_metadata(
157
132
  ex, video_path, filetype=types.FileType.VIDEO
158
133
  )
134
+ except exceptions.MapillaryUserError as ex:
135
+ # Considered as fatal error if not MapillaryDescriptionError
136
+ raise ex
159
137
  except Exception as ex:
138
+ # TODO: hide details if not verbose mode
160
139
  LOG.exception("Unexpected error extracting metadata from %s", video_path)
161
140
  return types.describe_error_metadata(
162
141
  ex, video_path, filetype=types.FileType.VIDEO
163
142
  )
143
+
144
+ def _generate_video_extractors(
145
+ self, video_paths: T.Sequence[Path]
146
+ ) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
147
+ raise NotImplementedError
@@ -6,16 +6,14 @@ import typing as T
6
6
  from pathlib import Path
7
7
 
8
8
  from .. import exceptions, types, utils
9
- from ..types import FileType
10
9
  from . import (
11
- geotag_from_generic,
10
+ base,
12
11
  geotag_images_from_exif,
13
12
  geotag_images_from_exiftool,
14
- geotag_images_from_exiftool_both_image_and_video,
15
13
  geotag_images_from_gpx_file,
16
14
  geotag_images_from_nmea_file,
17
15
  geotag_images_from_video,
18
- geotag_videos_from_exiftool_video,
16
+ geotag_videos_from_exiftool,
19
17
  geotag_videos_from_gpx,
20
18
  geotag_videos_from_video,
21
19
  )
@@ -71,8 +69,25 @@ def process(
71
69
  for idx, option in enumerate(options):
72
70
  LOG.debug("Processing %d files with %s", len(reprocessable_paths), option)
73
71
 
74
- image_metadata_or_errors = _geotag_images(reprocessable_paths, option)
75
- video_metadata_or_errors = _geotag_videos(reprocessable_paths, option)
72
+ image_videos, video_paths = _filter_images_and_videos(
73
+ reprocessable_paths, option.filetypes
74
+ )
75
+
76
+ if image_videos:
77
+ image_geotag = _build_image_geotag(option)
78
+ image_metadata_or_errors = (
79
+ image_geotag.to_description(image_videos) if image_geotag else []
80
+ )
81
+ else:
82
+ image_metadata_or_errors = []
83
+
84
+ if video_paths:
85
+ video_geotag = _build_video_geotag(option)
86
+ video_metadata_or_errors = (
87
+ video_geotag.to_description(video_paths) if video_geotag else []
88
+ )
89
+ else:
90
+ video_metadata_or_errors = []
76
91
 
77
92
  more_option = idx < len(options) - 1
78
93
 
@@ -98,6 +113,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
98
113
  (
99
114
  exceptions.MapillaryGeoTaggingError,
100
115
  exceptions.MapillaryVideoGPSNotFoundError,
116
+ exceptions.MapillaryExiftoolNotFoundError,
101
117
  ),
102
118
  ):
103
119
  return True
@@ -106,7 +122,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
106
122
 
107
123
 
108
124
  def _filter_images_and_videos(
109
- file_paths: T.Iterable[Path],
125
+ paths: T.Iterable[Path],
110
126
  filetypes: set[types.FileType] | None = None,
111
127
  ) -> tuple[list[Path], list[Path]]:
112
128
  image_paths = []
@@ -121,7 +137,7 @@ def _filter_images_and_videos(
121
137
  include_images = types.FileType.IMAGE in filetypes
122
138
  include_videos = bool(filetypes & ALL_VIDEO_TYPES)
123
139
 
124
- for path in file_paths:
140
+ for path in paths:
125
141
  if utils.is_image_file(path):
126
142
  if include_images:
127
143
  image_paths.append(path)
@@ -141,158 +157,144 @@ def _ensure_source_path(option: SourceOption) -> Path:
141
157
  return option.source_path.source_path
142
158
 
143
159
 
144
- def _geotag_images(
145
- paths: T.Iterable[Path], option: SourceOption
146
- ) -> list[types.ImageMetadataOrError]:
147
- image_paths, _ = _filter_images_and_videos(paths, option.filetypes)
148
-
149
- if not image_paths:
150
- return []
151
-
160
+ def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
161
+ """
162
+ Build a GeotagImagesFromGeneric object based on the provided option.
163
+ """
152
164
  if option.interpolation is None:
153
165
  interpolation = InterpolationOption()
154
166
  else:
155
167
  interpolation = option.interpolation
156
168
 
157
- geotag: geotag_from_generic.GeotagImagesFromGeneric
158
-
159
- if option.source is SourceType.NATIVE:
160
- geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
161
- image_paths, num_processes=option.num_processes
169
+ if option.source in [SourceType.EXIF, SourceType.NATIVE]:
170
+ return geotag_images_from_exif.GeotagImagesFromEXIF(
171
+ num_processes=option.num_processes
162
172
  )
163
- return geotag.to_description()
164
173
 
165
174
  if option.source is SourceType.EXIFTOOL_RUNTIME:
166
- geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
167
- image_paths, num_processes=option.num_processes
175
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
176
+ num_processes=option.num_processes
168
177
  )
169
- try:
170
- return geotag.to_description()
171
- except exceptions.MapillaryExiftoolNotFoundError as ex:
172
- LOG.warning('Skip "%s" because: %s', option.source.value, ex)
173
- return []
174
178
 
175
179
  elif option.source is SourceType.EXIFTOOL_XML:
176
180
  # This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
177
181
  # to work
178
- geotag = geotag_images_from_exiftool_both_image_and_video.GeotagImagesFromExifToolBothImageAndVideo(
179
- image_paths,
180
- xml_path=_ensure_source_path(option),
182
+ if option.source_path is None:
183
+ raise exceptions.MapillaryBadParameterError(
184
+ "source_path must be provided for EXIFTOOL_XML source"
185
+ )
186
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
187
+ source_path=option.source_path,
181
188
  num_processes=option.num_processes,
182
189
  )
183
- return geotag.to_description()
184
190
 
185
191
  elif option.source is SourceType.GPX:
186
- geotag = geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
187
- image_paths,
192
+ return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
188
193
  source_path=_ensure_source_path(option),
189
194
  use_gpx_start_time=interpolation.use_gpx_start_time,
190
195
  offset_time=interpolation.offset_time,
191
196
  num_processes=option.num_processes,
192
197
  )
193
- return geotag.to_description()
194
198
 
195
199
  elif option.source is SourceType.NMEA:
196
- geotag = geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
197
- image_paths,
200
+ return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
198
201
  source_path=_ensure_source_path(option),
199
202
  use_gpx_start_time=interpolation.use_gpx_start_time,
200
203
  offset_time=interpolation.offset_time,
201
204
  num_processes=option.num_processes,
202
205
  )
203
206
 
204
- return geotag.to_description()
205
-
206
- elif option.source is SourceType.EXIF:
207
- geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
208
- image_paths, num_processes=option.num_processes
209
- )
210
- return geotag.to_description()
211
-
212
- elif option.source in [
213
- SourceType.GOPRO,
214
- SourceType.BLACKVUE,
215
- SourceType.CAMM,
216
- ]:
217
- map_geotag_source_to_filetype: dict[SourceType, FileType] = {
218
- SourceType.GOPRO: FileType.GOPRO,
219
- SourceType.BLACKVUE: FileType.BLACKVUE,
220
- SourceType.CAMM: FileType.CAMM,
221
- }
222
- video_paths = utils.find_videos([_ensure_source_path(option)])
223
- image_samples_by_video_path = utils.find_all_image_samples(
224
- image_paths, video_paths
225
- )
226
- video_paths_with_image_samples = list(image_samples_by_video_path.keys())
227
- video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
228
- video_paths_with_image_samples,
229
- filetypes={map_geotag_source_to_filetype[option.source]},
230
- num_processes=option.num_processes,
231
- ).to_description()
232
- geotag = geotag_images_from_video.GeotagImagesFromVideo(
233
- image_paths,
234
- video_metadatas,
207
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
208
+ return geotag_images_from_video.GeotagImageSamplesFromVideo(
209
+ _ensure_source_path(option),
235
210
  offset_time=interpolation.offset_time,
236
211
  num_processes=option.num_processes,
237
212
  )
238
- return geotag.to_description()
239
213
 
240
214
  else:
241
215
  raise ValueError(f"Invalid geotag source {option.source}")
242
216
 
243
217
 
244
- def _geotag_videos(
245
- paths: T.Iterable[Path], option: SourceOption
246
- ) -> list[types.VideoMetadataOrError]:
247
- _, video_paths = _filter_images_and_videos(paths, option.filetypes)
248
-
249
- if not video_paths:
250
- return []
251
-
252
- geotag: geotag_from_generic.GeotagVideosFromGeneric
218
+ def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
219
+ """
220
+ Build a GeotagVideosFromGeneric object based on the provided option.
253
221
 
222
+ Examples:
223
+ >>> from pathlib import Path
224
+ >>> from mapillary_tools.geotag.options import SourceOption, SourceType
225
+ >>> opt = SourceOption(SourceType.NATIVE)
226
+ >>> geotagger = _build_video_geotag(opt)
227
+ >>> geotagger.__class__.__name__
228
+ 'GeotagVideosFromVideo'
229
+
230
+ >>> opt = SourceOption(SourceType.EXIFTOOL_RUNTIME)
231
+ >>> geotagger = _build_video_geotag(opt)
232
+ >>> geotagger.__class__.__name__
233
+ 'GeotagVideosFromExifToolRunner'
234
+
235
+ >>> opt = SourceOption(SourceType.EXIFTOOL_XML, source_path=Path("/tmp/test.xml"))
236
+ >>> geotagger = _build_video_geotag(opt)
237
+ >>> geotagger.__class__.__name__
238
+ 'GeotagVideosFromExifToolXML'
239
+
240
+ >>> opt = SourceOption(SourceType.GPX, source_path=Path("/tmp/test.gpx"))
241
+ >>> geotagger = _build_video_geotag(opt)
242
+ >>> geotagger.__class__.__name__
243
+ 'GeotagVideosFromGPX'
244
+
245
+ >>> opt = SourceOption(SourceType.NMEA, source_path=Path("/tmp/test.nmea"))
246
+ >>> _build_video_geotag(opt) is None
247
+ True
248
+
249
+ >>> opt = SourceOption(SourceType.EXIF)
250
+ >>> _build_video_geotag(opt) is None
251
+ True
252
+
253
+ >>> opt = SourceOption(SourceType.GOPRO)
254
+ >>> _build_video_geotag(opt) is None
255
+ True
256
+
257
+ >>> try:
258
+ ... _build_video_geotag(SourceOption("invalid"))
259
+ ... except ValueError as e:
260
+ ... "Invalid geotag source" in str(e)
261
+ True
262
+ """
254
263
  if option.source is SourceType.NATIVE:
255
- geotag = geotag_videos_from_video.GeotagVideosFromVideo(
256
- video_paths, num_processes=option.num_processes, filetypes=option.filetypes
264
+ return geotag_videos_from_video.GeotagVideosFromVideo(
265
+ num_processes=option.num_processes, filetypes=option.filetypes
257
266
  )
258
- return geotag.to_description()
259
267
 
260
268
  if option.source is SourceType.EXIFTOOL_RUNTIME:
261
- geotag = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolRunner(
262
- video_paths, num_processes=option.num_processes
269
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
270
+ num_processes=option.num_processes
263
271
  )
264
- try:
265
- return geotag.to_description()
266
- except exceptions.MapillaryExiftoolNotFoundError as ex:
267
- LOG.warning('Skip "%s" because: %s', option.source.value, ex)
268
- return []
269
272
 
270
273
  elif option.source is SourceType.EXIFTOOL_XML:
271
- geotag = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
272
- video_paths,
273
- xml_path=_ensure_source_path(option),
274
+ if option.source_path is None:
275
+ raise exceptions.MapillaryBadParameterError(
276
+ "source_path must be provided for EXIFTOOL_XML source"
277
+ )
278
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
279
+ source_path=option.source_path,
274
280
  )
275
- return geotag.to_description()
276
281
 
277
282
  elif option.source is SourceType.GPX:
278
- geotag = geotag_videos_from_gpx.GeotagVideosFromGPX(video_paths)
279
- return geotag.to_description()
283
+ return geotag_videos_from_gpx.GeotagVideosFromGPX(
284
+ source_path=option.source_path, num_processes=option.num_processes
285
+ )
280
286
 
281
287
  elif option.source is SourceType.NMEA:
282
288
  # TODO: geotag videos from NMEA
283
- return []
289
+ return None
284
290
 
285
291
  elif option.source is SourceType.EXIF:
286
292
  # Legacy image-specific geotag types
287
- return []
293
+ return None
288
294
 
289
- elif option.source in [
290
- SourceType.GOPRO,
291
- SourceType.BLACKVUE,
292
- SourceType.CAMM,
293
- ]:
295
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
294
296
  # Legacy image-specific geotag types
295
- return []
297
+ return None
296
298
 
297
299
  else:
298
300
  raise ValueError(f"Invalid geotag source {option.source}")
@@ -1,60 +1,24 @@
1
- import contextlib
1
+ from __future__ import annotations
2
+
2
3
  import logging
4
+ import sys
3
5
  import typing as T
4
6
  from pathlib import Path
5
7
 
6
- from .. import exceptions, geo, types, utils
7
- from ..exif_read import ExifRead, ExifReadABC
8
- from .geotag_from_generic import GenericImageExtractor, GeotagImagesFromGeneric
9
-
10
- LOG = logging.getLogger(__name__)
11
-
12
-
13
- class ImageEXIFExtractor(GenericImageExtractor):
14
- def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
15
- super().__init__(image_path)
16
- self.skip_lonlat_error = skip_lonlat_error
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
17
12
 
18
- @contextlib.contextmanager
19
- def _exif_context(self) -> T.Generator[ExifReadABC, None, None]:
20
- with self.image_path.open("rb") as fp:
21
- yield ExifRead(fp)
13
+ from .base import GeotagImagesFromGeneric
14
+ from .image_extractors.exif import ImageEXIFExtractor
22
15
 
23
- def extract(self) -> types.ImageMetadata:
24
- with self._exif_context() as exif:
25
- lonlat = exif.extract_lon_lat()
26
- if lonlat is None:
27
- if not self.skip_lonlat_error:
28
- raise exceptions.MapillaryGeoTaggingError(
29
- "Unable to extract GPS Longitude or GPS Latitude from the image"
30
- )
31
- lonlat = (0.0, 0.0)
32
- lon, lat = lonlat
33
-
34
- capture_time = exif.extract_capture_time()
35
- if capture_time is None:
36
- raise exceptions.MapillaryGeoTaggingError(
37
- "Unable to extract timestamp from the image"
38
- )
39
-
40
- image_metadata = types.ImageMetadata(
41
- filename=self.image_path,
42
- filesize=utils.get_file_size(self.image_path),
43
- time=geo.as_unix_time(capture_time),
44
- lat=lat,
45
- lon=lon,
46
- alt=exif.extract_altitude(),
47
- angle=exif.extract_direction(),
48
- width=exif.extract_width(),
49
- height=exif.extract_height(),
50
- MAPOrientation=exif.extract_orientation(),
51
- MAPDeviceMake=exif.extract_make(),
52
- MAPDeviceModel=exif.extract_model(),
53
- )
54
-
55
- return image_metadata
16
+ LOG = logging.getLogger(__name__)
56
17
 
57
18
 
58
19
  class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
59
- def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
60
- return [ImageEXIFExtractor(path) for path in self.image_paths]
20
+ @override
21
+ def _generate_image_extractors(
22
+ self, image_paths: T.Sequence[Path]
23
+ ) -> T.Sequence[ImageEXIFExtractor]:
24
+ return [ImageEXIFExtractor(path) for path in image_paths]