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.
Files changed (87) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +287 -22
  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 +17 -8
  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 +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 +408 -416
  61. mapillary_tools/upload_api_v4.py +172 -174
  62. mapillary_tools/uploader.py +804 -284
  63. mapillary_tools/utils.py +49 -18
  64. {mapillary_tools-0.13.3a1.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.3a1.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.3a1.dist-info/RECORD +0 -75
  84. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  85. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
  86. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
  87. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
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 . import (
10
+ base,
11
+ geotag_images_from_exif,
12
+ geotag_images_from_exiftool,
13
+ geotag_images_from_gpx_file,
14
+ geotag_images_from_nmea_file,
15
+ geotag_images_from_video,
16
+ geotag_videos_from_exiftool,
17
+ geotag_videos_from_gpx,
18
+ geotag_videos_from_video,
19
+ )
20
+ from .options import InterpolationOption, SOURCE_TYPE_ALIAS, SourceOption, SourceType
21
+
22
+
23
+ LOG = logging.getLogger(__name__)
24
+
25
+
26
+ def parse_source_option(source: str) -> list[SourceOption]:
27
+ """
28
+ Given a source string, parse it into a list of GeotagOptions objects.
29
+
30
+ Examples:
31
+ "native" -> [SourceOption(SourceType.NATIVE)]
32
+ "gpx,exif" -> [SourceOption(SourceType.GPX), SourceOption(SourceType.EXIF)]
33
+ "exif,gpx" -> [SourceOption(SourceType.EXIF), SourceOption(SourceType.GPX)]
34
+ '{"source": "gpx"}' -> [SourceOption(SourceType.GPX)]
35
+ """
36
+
37
+ try:
38
+ source_type = SourceType(SOURCE_TYPE_ALIAS.get(source, source))
39
+ except ValueError:
40
+ pass
41
+ else:
42
+ return [SourceOption(source_type)]
43
+
44
+ try:
45
+ payload = json.loads(source)
46
+ except json.JSONDecodeError:
47
+ pass
48
+ else:
49
+ return [SourceOption.from_dict(payload)]
50
+
51
+ sources = source.split(",")
52
+
53
+ return [SourceOption(SourceType(SOURCE_TYPE_ALIAS.get(s, s))) for s in sources]
54
+
55
+
56
+ def process(
57
+ # Collection: ABC for sized iterable container classes
58
+ paths: T.Iterable[Path],
59
+ options: T.Collection[SourceOption],
60
+ ) -> list[types.MetadataOrError]:
61
+ if not options:
62
+ raise ValueError("No geotag options provided")
63
+
64
+ final_metadatas: list[types.MetadataOrError] = []
65
+
66
+ # Paths (image path or video path) that will be sent to the next geotag process
67
+ reprocessable_paths = set(paths)
68
+
69
+ for idx, option in enumerate(options):
70
+ if LOG.getEffectiveLevel() <= logging.DEBUG:
71
+ LOG.info(
72
+ f"==> Processing {len(reprocessable_paths)} files with source {option}..."
73
+ )
74
+ else:
75
+ LOG.info(
76
+ f"==> Processing {len(reprocessable_paths)} files with source {option.source.value}..."
77
+ )
78
+
79
+ image_videos, video_paths = _filter_images_and_videos(
80
+ reprocessable_paths, option.filetypes
81
+ )
82
+
83
+ if image_videos:
84
+ image_geotag = _build_image_geotag(option)
85
+ image_metadata_or_errors = (
86
+ image_geotag.to_description(image_videos) if image_geotag else []
87
+ )
88
+ else:
89
+ image_metadata_or_errors = []
90
+
91
+ if video_paths:
92
+ video_geotag = _build_video_geotag(option)
93
+ video_metadata_or_errors = (
94
+ video_geotag.to_description(video_paths) if video_geotag else []
95
+ )
96
+ else:
97
+ video_metadata_or_errors = []
98
+
99
+ more_option = idx < len(options) - 1
100
+
101
+ for metadata in image_metadata_or_errors + video_metadata_or_errors:
102
+ if more_option and _is_reprocessable(metadata):
103
+ # Leave what it is for the next geotag process
104
+ pass
105
+ else:
106
+ final_metadatas.append(metadata)
107
+ reprocessable_paths.remove(metadata.filename)
108
+
109
+ # Quit if no more paths to process
110
+ if not reprocessable_paths:
111
+ break
112
+
113
+ return final_metadatas
114
+
115
+
116
+ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
117
+ if isinstance(metadata, types.ErrorMetadata):
118
+ if isinstance(
119
+ metadata.error,
120
+ (
121
+ exceptions.MapillaryGeoTaggingError,
122
+ exceptions.MapillaryVideoGPSNotFoundError,
123
+ exceptions.MapillaryExiftoolNotFoundError,
124
+ ),
125
+ ):
126
+ return True
127
+
128
+ return False
129
+
130
+
131
+ def _filter_images_and_videos(
132
+ paths: T.Iterable[Path],
133
+ filetypes: set[types.FileType] | None = None,
134
+ ) -> tuple[list[Path], list[Path]]:
135
+ image_paths = []
136
+ video_paths = []
137
+
138
+ ALL_VIDEO_TYPES = {types.FileType.VIDEO, *types.NATIVE_VIDEO_FILETYPES}
139
+
140
+ if filetypes is None:
141
+ include_images = True
142
+ include_videos = True
143
+ else:
144
+ include_images = types.FileType.IMAGE in filetypes
145
+ include_videos = bool(filetypes & ALL_VIDEO_TYPES)
146
+
147
+ for path in paths:
148
+ if utils.is_image_file(path):
149
+ if include_images:
150
+ image_paths.append(path)
151
+
152
+ elif utils.is_video_file(path):
153
+ if include_videos:
154
+ video_paths.append(path)
155
+
156
+ return image_paths, video_paths
157
+
158
+
159
+ def _ensure_source_path(option: SourceOption) -> Path:
160
+ if option.source_path is None or option.source_path.source_path is None:
161
+ raise exceptions.MapillaryBadParameterError(
162
+ f"source_path must be provided for {option.source}"
163
+ )
164
+ return option.source_path.source_path
165
+
166
+
167
+ def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
168
+ """
169
+ Build a GeotagImagesFromGeneric object based on the provided option.
170
+ """
171
+ if option.interpolation is None:
172
+ interpolation = InterpolationOption()
173
+ else:
174
+ interpolation = option.interpolation
175
+
176
+ if option.source in [SourceType.EXIF, SourceType.NATIVE]:
177
+ return geotag_images_from_exif.GeotagImagesFromEXIF(
178
+ num_processes=option.num_processes
179
+ )
180
+
181
+ if option.source is SourceType.EXIFTOOL_RUNTIME:
182
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
183
+ num_processes=option.num_processes
184
+ )
185
+
186
+ elif option.source is SourceType.EXIFTOOL_XML:
187
+ # This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
188
+ # to work
189
+ if option.source_path is None:
190
+ raise exceptions.MapillaryBadParameterError(
191
+ "source_path must be provided for EXIFTOOL_XML source"
192
+ )
193
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
194
+ source_path=option.source_path,
195
+ num_processes=option.num_processes,
196
+ )
197
+
198
+ elif option.source is SourceType.GPX:
199
+ return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
200
+ source_path=_ensure_source_path(option),
201
+ use_gpx_start_time=interpolation.use_gpx_start_time,
202
+ offset_time=interpolation.offset_time,
203
+ num_processes=option.num_processes,
204
+ )
205
+
206
+ elif option.source is SourceType.NMEA:
207
+ return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
208
+ source_path=_ensure_source_path(option),
209
+ use_gpx_start_time=interpolation.use_gpx_start_time,
210
+ offset_time=interpolation.offset_time,
211
+ num_processes=option.num_processes,
212
+ )
213
+
214
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
215
+ return geotag_images_from_video.GeotagImageSamplesFromVideo(
216
+ _ensure_source_path(option),
217
+ offset_time=interpolation.offset_time,
218
+ num_processes=option.num_processes,
219
+ )
220
+
221
+ else:
222
+ raise ValueError(f"Invalid geotag source {option.source}")
223
+
224
+
225
+ def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
226
+ """
227
+ Build a GeotagVideosFromGeneric object based on the provided option.
228
+
229
+ Examples:
230
+ >>> from pathlib import Path
231
+ >>> from mapillary_tools.geotag.options import SourceOption, SourceType
232
+ >>> opt = SourceOption(SourceType.NATIVE)
233
+ >>> geotagger = _build_video_geotag(opt)
234
+ >>> geotagger.__class__.__name__
235
+ 'GeotagVideosFromVideo'
236
+
237
+ >>> opt = SourceOption(SourceType.EXIFTOOL_RUNTIME)
238
+ >>> geotagger = _build_video_geotag(opt)
239
+ >>> geotagger.__class__.__name__
240
+ 'GeotagVideosFromExifToolRunner'
241
+
242
+ >>> opt = SourceOption(SourceType.EXIFTOOL_XML, source_path=Path("/tmp/test.xml"))
243
+ >>> geotagger = _build_video_geotag(opt)
244
+ >>> geotagger.__class__.__name__
245
+ 'GeotagVideosFromExifToolXML'
246
+
247
+ >>> opt = SourceOption(SourceType.GPX, source_path=Path("/tmp/test.gpx"))
248
+ >>> geotagger = _build_video_geotag(opt)
249
+ >>> geotagger.__class__.__name__
250
+ 'GeotagVideosFromGPX'
251
+
252
+ >>> opt = SourceOption(SourceType.NMEA, source_path=Path("/tmp/test.nmea"))
253
+ >>> _build_video_geotag(opt) is None
254
+ True
255
+
256
+ >>> opt = SourceOption(SourceType.EXIF)
257
+ >>> _build_video_geotag(opt) is None
258
+ True
259
+
260
+ >>> opt = SourceOption(SourceType.GOPRO)
261
+ >>> _build_video_geotag(opt) is None
262
+ True
263
+
264
+ >>> try:
265
+ ... _build_video_geotag(SourceOption("invalid"))
266
+ ... except ValueError as e:
267
+ ... "Invalid geotag source" in str(e)
268
+ True
269
+ """
270
+ if option.source is SourceType.NATIVE:
271
+ return geotag_videos_from_video.GeotagVideosFromVideo(
272
+ num_processes=option.num_processes, filetypes=option.filetypes
273
+ )
274
+
275
+ if option.source is SourceType.EXIFTOOL_RUNTIME:
276
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
277
+ num_processes=option.num_processes
278
+ )
279
+
280
+ elif option.source is SourceType.EXIFTOOL_XML:
281
+ if option.source_path is None:
282
+ raise exceptions.MapillaryBadParameterError(
283
+ "source_path must be provided for EXIFTOOL_XML source"
284
+ )
285
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
286
+ source_path=option.source_path,
287
+ )
288
+
289
+ elif option.source is SourceType.GPX:
290
+ return geotag_videos_from_gpx.GeotagVideosFromGPX(
291
+ source_path=option.source_path, num_processes=option.num_processes
292
+ )
293
+
294
+ elif option.source is SourceType.NMEA:
295
+ # TODO: geotag videos from NMEA
296
+ return None
297
+
298
+ elif option.source is SourceType.EXIF:
299
+ # Legacy image-specific geotag types
300
+ return None
301
+
302
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
303
+ # Legacy image-specific geotag types
304
+ return None
305
+
306
+ else:
307
+ 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]