mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +287 -22
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +17 -8
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +408 -416
- mapillary_tools/upload_api_v4.py +172 -174
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
-
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
8
12
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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]
|