mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__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 (83) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +237 -16
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +429 -181
  7. mapillary_tools/commands/__main__.py +12 -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 +19 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +31 -13
  15. mapillary_tools/constants.py +47 -6
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +221 -116
  18. mapillary_tools/exif_write.py +7 -7
  19. mapillary_tools/exiftool_read.py +33 -42
  20. mapillary_tools/exiftool_read_video.py +46 -33
  21. mapillary_tools/exiftool_runner.py +77 -0
  22. mapillary_tools/ffmpeg.py +24 -23
  23. mapillary_tools/geo.py +144 -120
  24. mapillary_tools/geotag/base.py +147 -0
  25. mapillary_tools/geotag/factory.py +291 -0
  26. mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
  27. mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
  28. mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
  29. mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
  30. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
  31. mapillary_tools/geotag/geotag_images_from_video.py +53 -51
  32. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  33. mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
  34. mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
  35. mapillary_tools/geotag/image_extractors/base.py +18 -0
  36. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  37. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  38. mapillary_tools/geotag/options.py +160 -0
  39. mapillary_tools/geotag/utils.py +52 -16
  40. mapillary_tools/geotag/video_extractors/base.py +18 -0
  41. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  42. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  43. mapillary_tools/geotag/video_extractors/native.py +157 -0
  44. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
  45. mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
  46. mapillary_tools/history.py +7 -13
  47. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  48. mapillary_tools/mp4/io_utils.py +0 -1
  49. mapillary_tools/mp4/mp4_sample_parser.py +36 -28
  50. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  51. mapillary_tools/mp4/simple_mp4_parser.py +13 -22
  52. mapillary_tools/process_geotag_properties.py +155 -392
  53. mapillary_tools/process_sequence_properties.py +562 -208
  54. mapillary_tools/sample_video.py +13 -20
  55. mapillary_tools/telemetry.py +26 -13
  56. mapillary_tools/types.py +111 -58
  57. mapillary_tools/upload.py +316 -298
  58. mapillary_tools/upload_api_v4.py +55 -122
  59. mapillary_tools/uploader.py +396 -254
  60. mapillary_tools/utils.py +42 -18
  61. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
  62. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
  64. mapillary_tools/geotag/__init__.py +0 -1
  65. mapillary_tools/geotag/geotag_from_generic.py +0 -22
  66. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
  67. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
  68. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  69. mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
  70. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  71. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
  72. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
  73. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
  74. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
  75. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  76. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
  77. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  78. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  79. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  80. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  81. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  82. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
  83. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
@@ -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 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 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
@@ -0,0 +1,291 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import typing as T
6
+ from pathlib import Path
7
+
8
+ from .. import exceptions, types, utils
9
+ from ..types import FileType
10
+ from . import (
11
+ base,
12
+ geotag_images_from_exif,
13
+ geotag_images_from_exiftool,
14
+ geotag_images_from_gpx_file,
15
+ geotag_images_from_nmea_file,
16
+ geotag_images_from_video,
17
+ geotag_videos_from_exiftool,
18
+ geotag_videos_from_gpx,
19
+ geotag_videos_from_video,
20
+ )
21
+ from .options import InterpolationOption, SOURCE_TYPE_ALIAS, SourceOption, SourceType
22
+
23
+
24
+ LOG = logging.getLogger(__name__)
25
+
26
+
27
+ def parse_source_option(source: str) -> list[SourceOption]:
28
+ """
29
+ Given a source string, parse it into a list of GeotagOptions objects.
30
+
31
+ Examples:
32
+ "native" -> [SourceOption(SourceType.NATIVE)]
33
+ "gpx,exif" -> [SourceOption(SourceType.GPX), SourceOption(SourceType.EXIF)]
34
+ "exif,gpx" -> [SourceOption(SourceType.EXIF), SourceOption(SourceType.GPX)]
35
+ '{"source": "gpx"}' -> [SourceOption(SourceType.GPX)]
36
+ """
37
+
38
+ try:
39
+ source_type = SourceType(SOURCE_TYPE_ALIAS.get(source, source))
40
+ except ValueError:
41
+ pass
42
+ else:
43
+ return [SourceOption(source_type)]
44
+
45
+ try:
46
+ payload = json.loads(source)
47
+ except json.JSONDecodeError:
48
+ pass
49
+ else:
50
+ return [SourceOption.from_dict(payload)]
51
+
52
+ sources = source.split(",")
53
+
54
+ return [SourceOption(SourceType(SOURCE_TYPE_ALIAS.get(s, s))) for s in sources]
55
+
56
+
57
+ def process(
58
+ # Collection: ABC for sized iterable container classes
59
+ paths: T.Iterable[Path],
60
+ options: T.Collection[SourceOption],
61
+ ) -> list[types.MetadataOrError]:
62
+ if not options:
63
+ raise ValueError("No geotag options provided")
64
+
65
+ final_metadatas: list[types.MetadataOrError] = []
66
+
67
+ # Paths (image path or video path) that will be sent to the next geotag process
68
+ reprocessable_paths = set(paths)
69
+
70
+ for idx, option in enumerate(options):
71
+ LOG.debug("Processing %d files with %s", len(reprocessable_paths), option)
72
+
73
+ image_metadata_or_errors = _geotag_images(reprocessable_paths, option)
74
+ video_metadata_or_errors = _geotag_videos(reprocessable_paths, option)
75
+
76
+ more_option = idx < len(options) - 1
77
+
78
+ for metadata in image_metadata_or_errors + video_metadata_or_errors:
79
+ if more_option and _is_reprocessable(metadata):
80
+ # Leave what it is for the next geotag process
81
+ pass
82
+ else:
83
+ final_metadatas.append(metadata)
84
+ reprocessable_paths.remove(metadata.filename)
85
+
86
+ # Quit if no more paths to process
87
+ if not reprocessable_paths:
88
+ break
89
+
90
+ return final_metadatas
91
+
92
+
93
+ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
94
+ if isinstance(metadata, types.ErrorMetadata):
95
+ if isinstance(
96
+ metadata.error,
97
+ (
98
+ exceptions.MapillaryGeoTaggingError,
99
+ exceptions.MapillaryVideoGPSNotFoundError,
100
+ ),
101
+ ):
102
+ return True
103
+
104
+ return False
105
+
106
+
107
+ def _filter_images_and_videos(
108
+ paths: T.Iterable[Path],
109
+ filetypes: set[types.FileType] | None = None,
110
+ ) -> tuple[list[Path], list[Path]]:
111
+ image_paths = []
112
+ video_paths = []
113
+
114
+ ALL_VIDEO_TYPES = {types.FileType.VIDEO, *types.NATIVE_VIDEO_FILETYPES}
115
+
116
+ if filetypes is None:
117
+ include_images = True
118
+ include_videos = True
119
+ else:
120
+ include_images = types.FileType.IMAGE in filetypes
121
+ include_videos = bool(filetypes & ALL_VIDEO_TYPES)
122
+
123
+ for path in paths:
124
+ if utils.is_image_file(path):
125
+ if include_images:
126
+ image_paths.append(path)
127
+
128
+ elif utils.is_video_file(path):
129
+ if include_videos:
130
+ video_paths.append(path)
131
+
132
+ return image_paths, video_paths
133
+
134
+
135
+ def _ensure_source_path(option: SourceOption) -> Path:
136
+ if option.source_path is None or option.source_path.source_path is None:
137
+ raise exceptions.MapillaryBadParameterError(
138
+ f"source_path must be provided for {option.source}"
139
+ )
140
+ return option.source_path.source_path
141
+
142
+
143
+ def _geotag_images(
144
+ paths: T.Iterable[Path], option: SourceOption
145
+ ) -> list[types.ImageMetadataOrError]:
146
+ image_paths, _ = _filter_images_and_videos(paths, option.filetypes)
147
+
148
+ if not image_paths:
149
+ return []
150
+
151
+ if option.interpolation is None:
152
+ interpolation = InterpolationOption()
153
+ else:
154
+ interpolation = option.interpolation
155
+
156
+ geotag: base.GeotagImagesFromGeneric
157
+
158
+ if option.source is SourceType.NATIVE:
159
+ geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
160
+ num_processes=option.num_processes
161
+ )
162
+ return geotag.to_description(image_paths)
163
+
164
+ if option.source is SourceType.EXIFTOOL_RUNTIME:
165
+ geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
166
+ num_processes=option.num_processes
167
+ )
168
+ try:
169
+ return geotag.to_description(image_paths)
170
+ except exceptions.MapillaryExiftoolNotFoundError as ex:
171
+ LOG.warning('Skip "%s" because: %s', option.source.value, ex)
172
+ return []
173
+
174
+ elif option.source is SourceType.EXIFTOOL_XML:
175
+ # This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
176
+ # to work
177
+ geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
178
+ xml_path=_ensure_source_path(option),
179
+ num_processes=option.num_processes,
180
+ )
181
+ return geotag.to_description(image_paths)
182
+
183
+ elif option.source is SourceType.GPX:
184
+ geotag = geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
185
+ source_path=_ensure_source_path(option),
186
+ use_gpx_start_time=interpolation.use_gpx_start_time,
187
+ offset_time=interpolation.offset_time,
188
+ num_processes=option.num_processes,
189
+ )
190
+ return geotag.to_description(image_paths)
191
+
192
+ elif option.source is SourceType.NMEA:
193
+ geotag = geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
194
+ source_path=_ensure_source_path(option),
195
+ use_gpx_start_time=interpolation.use_gpx_start_time,
196
+ offset_time=interpolation.offset_time,
197
+ num_processes=option.num_processes,
198
+ )
199
+
200
+ return geotag.to_description(image_paths)
201
+
202
+ elif option.source is SourceType.EXIF:
203
+ geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
204
+ num_processes=option.num_processes
205
+ )
206
+ return geotag.to_description(image_paths)
207
+
208
+ elif option.source in [
209
+ SourceType.GOPRO,
210
+ SourceType.BLACKVUE,
211
+ SourceType.CAMM,
212
+ ]:
213
+ map_geotag_source_to_filetype: dict[SourceType, FileType] = {
214
+ SourceType.GOPRO: FileType.GOPRO,
215
+ SourceType.BLACKVUE: FileType.BLACKVUE,
216
+ SourceType.CAMM: FileType.CAMM,
217
+ }
218
+ video_paths = utils.find_videos([_ensure_source_path(option)])
219
+ image_samples_by_video_path = utils.find_all_image_samples(
220
+ image_paths, video_paths
221
+ )
222
+ video_paths_with_image_samples = list(image_samples_by_video_path.keys())
223
+ video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
224
+ filetypes={map_geotag_source_to_filetype[option.source]},
225
+ num_processes=option.num_processes,
226
+ ).to_description(video_paths_with_image_samples)
227
+ geotag = geotag_images_from_video.GeotagImagesFromVideo(
228
+ video_metadatas,
229
+ offset_time=interpolation.offset_time,
230
+ num_processes=option.num_processes,
231
+ )
232
+ return geotag.to_description(image_paths)
233
+
234
+ else:
235
+ raise ValueError(f"Invalid geotag source {option.source}")
236
+
237
+
238
+ def _geotag_videos(
239
+ paths: T.Iterable[Path], option: SourceOption
240
+ ) -> list[types.VideoMetadataOrError]:
241
+ _, video_paths = _filter_images_and_videos(paths, option.filetypes)
242
+
243
+ if not video_paths:
244
+ return []
245
+
246
+ geotag: base.GeotagVideosFromGeneric
247
+
248
+ if option.source is SourceType.NATIVE:
249
+ geotag = geotag_videos_from_video.GeotagVideosFromVideo(
250
+ num_processes=option.num_processes, filetypes=option.filetypes
251
+ )
252
+ return geotag.to_description(video_paths)
253
+
254
+ if option.source is SourceType.EXIFTOOL_RUNTIME:
255
+ geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
256
+ num_processes=option.num_processes
257
+ )
258
+ try:
259
+ return geotag.to_description(video_paths)
260
+ except exceptions.MapillaryExiftoolNotFoundError as ex:
261
+ LOG.warning('Skip "%s" because: %s', option.source.value, ex)
262
+ return []
263
+
264
+ elif option.source is SourceType.EXIFTOOL_XML:
265
+ geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
266
+ xml_path=_ensure_source_path(option),
267
+ )
268
+ return geotag.to_description(video_paths)
269
+
270
+ elif option.source is SourceType.GPX:
271
+ geotag = geotag_videos_from_gpx.GeotagVideosFromGPX()
272
+ return geotag.to_description(video_paths)
273
+
274
+ elif option.source is SourceType.NMEA:
275
+ # TODO: geotag videos from NMEA
276
+ return []
277
+
278
+ elif option.source is SourceType.EXIF:
279
+ # Legacy image-specific geotag types
280
+ return []
281
+
282
+ elif option.source in [
283
+ SourceType.GOPRO,
284
+ SourceType.BLACKVUE,
285
+ SourceType.CAMM,
286
+ ]:
287
+ # Legacy image-specific geotag types
288
+ return []
289
+
290
+ else:
291
+ raise ValueError(f"Invalid geotag source {option.source}")
@@ -1,141 +1,24 @@
1
- import io
1
+ from __future__ import annotations
2
+
2
3
  import logging
4
+ import sys
3
5
  import typing as T
4
- from multiprocessing import Pool
5
6
  from pathlib import Path
6
7
 
7
- from tqdm import tqdm
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
8
12
 
9
- from .. import exceptions, exif_write, geo, types, utils
10
- from ..exif_read import ExifRead, ExifReadABC
11
- from .geotag_from_generic import GeotagImagesFromGeneric
13
+ from .base import GeotagImagesFromGeneric
14
+ from .image_extractors.exif import ImageEXIFExtractor
12
15
 
13
16
  LOG = logging.getLogger(__name__)
14
17
 
15
18
 
16
- def verify_image_exif_write(
17
- metadata: types.ImageMetadata,
18
- image_bytes: T.Optional[bytes] = None,
19
- ) -> None:
20
- if image_bytes is None:
21
- edit = exif_write.ExifEdit(metadata.filename)
22
- else:
23
- edit = exif_write.ExifEdit(image_bytes)
24
-
25
- # The cast is to fix the type error in Python3.6:
26
- # Argument 1 to "add_image_description" of "ExifEdit" has incompatible type "ImageDescription"; expected "Dict[str, Any]"
27
- edit.add_image_description(
28
- T.cast(T.Dict, types.desc_file_to_exif(types.as_desc(metadata)))
29
- )
30
-
31
- # Possible errors thrown here:
32
- # - struct.error: 'H' format requires 0 <= number <= 65535
33
- # - piexif.InvalidImageDataError
34
- edit.dump_image_bytes()
35
-
36
-
37
19
  class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
38
- def __init__(
39
- self, image_paths: T.Sequence[Path], num_processes: T.Optional[int] = None
40
- ):
41
- self.image_paths = image_paths
42
- self.num_processes = num_processes
43
- super().__init__()
44
-
45
- @staticmethod
46
- def build_image_metadata(
47
- image_path: Path, exif: ExifReadABC, skip_lonlat_error: bool = False
48
- ) -> types.ImageMetadata:
49
- lonlat = exif.extract_lon_lat()
50
- if lonlat is None:
51
- if not skip_lonlat_error:
52
- raise exceptions.MapillaryGeoTaggingError(
53
- "Unable to extract GPS Longitude or GPS Latitude from the image"
54
- )
55
- lonlat = (0.0, 0.0)
56
- lon, lat = lonlat
57
-
58
- capture_time = exif.extract_capture_time()
59
- if capture_time is None:
60
- raise exceptions.MapillaryGeoTaggingError(
61
- "Unable to extract timestamp from the image"
62
- )
63
-
64
- image_metadata = types.ImageMetadata(
65
- filename=image_path,
66
- md5sum=None,
67
- filesize=utils.get_file_size(image_path),
68
- time=geo.as_unix_time(capture_time),
69
- lat=lat,
70
- lon=lon,
71
- alt=exif.extract_altitude(),
72
- angle=exif.extract_direction(),
73
- width=exif.extract_width(),
74
- height=exif.extract_height(),
75
- MAPOrientation=exif.extract_orientation(),
76
- MAPDeviceMake=exif.extract_make(),
77
- MAPDeviceModel=exif.extract_model(),
78
- )
79
-
80
- return image_metadata
81
-
82
- @staticmethod
83
- def geotag_image(
84
- image_path: Path, skip_lonlat_error: bool = False
85
- ) -> types.ImageMetadataOrError:
86
- try:
87
- # load the image bytes into memory to avoid reading it multiple times
88
- with image_path.open("rb") as fp:
89
- image_bytesio = io.BytesIO(fp.read())
90
-
91
- image_bytesio.seek(0, io.SEEK_SET)
92
- exif = ExifRead(image_bytesio)
93
-
94
- image_metadata = GeotagImagesFromEXIF.build_image_metadata(
95
- image_path, exif, skip_lonlat_error=skip_lonlat_error
96
- )
97
-
98
- image_bytesio.seek(0, io.SEEK_SET)
99
- verify_image_exif_write(
100
- image_metadata,
101
- image_bytes=image_bytesio.read(),
102
- )
103
- except Exception as ex:
104
- return types.describe_error_metadata(
105
- ex, image_path, filetype=types.FileType.IMAGE
106
- )
107
-
108
- image_bytesio.seek(0, io.SEEK_SET)
109
- image_metadata.update_md5sum(image_bytesio)
110
-
111
- return image_metadata
112
-
113
- def to_description(self) -> T.List[types.ImageMetadataOrError]:
114
- if self.num_processes is None:
115
- num_processes = self.num_processes
116
- disable_multiprocessing = False
117
- else:
118
- num_processes = max(self.num_processes, 1)
119
- disable_multiprocessing = self.num_processes <= 0
120
-
121
- with Pool(processes=num_processes) as pool:
122
- image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
123
- if disable_multiprocessing:
124
- image_metadatas_iter = map(
125
- GeotagImagesFromEXIF.geotag_image,
126
- self.image_paths,
127
- )
128
- else:
129
- image_metadatas_iter = pool.imap(
130
- GeotagImagesFromEXIF.geotag_image,
131
- self.image_paths,
132
- )
133
- return list(
134
- tqdm(
135
- image_metadatas_iter,
136
- desc="Extracting geotags from images",
137
- unit="images",
138
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
139
- total=len(self.image_paths),
140
- )
141
- )
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]