mapillary-tools 0.14.0a2__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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +1 -0
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/config.py +38 -17
- mapillary_tools/constants.py +2 -0
- mapillary_tools/exiftool_read_video.py +52 -15
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +406 -232
- mapillary_tools/geotag/__init__.py +0 -0
- mapillary_tools/geotag/base.py +2 -2
- mapillary_tools/geotag/factory.py +97 -88
- mapillary_tools/geotag/geotag_images_from_exiftool.py +26 -19
- mapillary_tools/geotag/geotag_images_from_gpx.py +13 -6
- mapillary_tools/geotag/geotag_images_from_video.py +35 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +39 -13
- mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
- mapillary_tools/geotag/options.py +25 -3
- mapillary_tools/geotag/video_extractors/base.py +1 -1
- mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
- mapillary_tools/geotag/video_extractors/gpx.py +60 -70
- mapillary_tools/geotag/video_extractors/native.py +9 -31
- mapillary_tools/history.py +4 -1
- mapillary_tools/process_geotag_properties.py +16 -8
- mapillary_tools/process_sequence_properties.py +9 -11
- mapillary_tools/sample_video.py +7 -6
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/types.py +44 -610
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/RECORD +38 -35
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
mapillary_tools/types.py
CHANGED
|
@@ -1,35 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import abc
|
|
3
4
|
import dataclasses
|
|
4
|
-
import datetime
|
|
5
5
|
import enum
|
|
6
6
|
import hashlib
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
7
|
import typing as T
|
|
10
8
|
import uuid
|
|
11
9
|
from pathlib import Path
|
|
12
|
-
from typing import Literal, TypedDict
|
|
13
10
|
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
from . import exceptions, geo, utils
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# http://wiki.gis.com/wiki/index.php/Decimal_degrees
|
|
20
|
-
# decimal places degrees distance
|
|
21
|
-
# 0 1.0 111 km
|
|
22
|
-
# 1 0.1 11.1 km
|
|
23
|
-
# 2 0.01 1.11 km
|
|
24
|
-
# 3 0.001 111 m
|
|
25
|
-
# 4 0.0001 11.1 m
|
|
26
|
-
# 5 0.00001 1.11 m
|
|
27
|
-
# 6 0.000001 0.111 m
|
|
28
|
-
# 7 0.0000001 1.11 cm
|
|
29
|
-
# 8 0.00000001 1.11 mm
|
|
30
|
-
_COORDINATES_PRECISION = 7
|
|
31
|
-
_ALTITUDE_PRECISION = 3
|
|
32
|
-
_ANGLE_PRECISION = 3
|
|
11
|
+
from . import geo, utils
|
|
33
12
|
|
|
34
13
|
|
|
35
14
|
class FileType(enum.Enum):
|
|
@@ -53,9 +32,13 @@ NATIVE_VIDEO_FILETYPES = {
|
|
|
53
32
|
@dataclasses.dataclass
|
|
54
33
|
class ImageMetadata(geo.Point):
|
|
55
34
|
filename: Path
|
|
35
|
+
# filetype should be always FileType.IMAGE
|
|
56
36
|
md5sum: str | None = None
|
|
57
37
|
width: int | None = None
|
|
58
38
|
height: int | None = None
|
|
39
|
+
filesize: int | None = None
|
|
40
|
+
|
|
41
|
+
# Fields starting with MAP* will be written to the image EXIF
|
|
59
42
|
MAPSequenceUUID: str | None = None
|
|
60
43
|
MAPDeviceMake: str | None = None
|
|
61
44
|
MAPDeviceModel: str | None = None
|
|
@@ -64,7 +47,6 @@ class ImageMetadata(geo.Point):
|
|
|
64
47
|
MAPOrientation: int | None = None
|
|
65
48
|
MAPMetaTags: dict | None = None
|
|
66
49
|
MAPFilename: str | None = None
|
|
67
|
-
filesize: int | None = None
|
|
68
50
|
|
|
69
51
|
def update_md5sum(self, image_data: T.BinaryIO | None = None) -> None:
|
|
70
52
|
if self.md5sum is None:
|
|
@@ -110,17 +92,47 @@ Metadata = T.Union[ImageMetadata, VideoMetadata]
|
|
|
110
92
|
MetadataOrError = T.Union[Metadata, ErrorMetadata]
|
|
111
93
|
|
|
112
94
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
95
|
+
class BaseSerializer(abc.ABC):
|
|
96
|
+
@classmethod
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def serialize(cls, metadatas: T.Sequence[MetadataOrError]) -> bytes:
|
|
99
|
+
raise NotImplementedError()
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
@abc.abstractmethod
|
|
103
|
+
def deserialize(cls, data: bytes) -> list[Metadata]:
|
|
104
|
+
raise NotImplementedError()
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def deserialize_stream(cls, data: T.IO[bytes]) -> list[Metadata]:
|
|
108
|
+
return cls.deserialize(data.read())
|
|
109
|
+
|
|
110
|
+
|
|
121
111
|
def combine_filetype_filters(
|
|
122
112
|
a: set[FileType] | None, b: set[FileType] | None
|
|
123
113
|
) -> set[FileType] | None:
|
|
114
|
+
"""
|
|
115
|
+
>>> combine_filetype_filters({FileType.CAMM}, {FileType.GOPRO})
|
|
116
|
+
set()
|
|
117
|
+
|
|
118
|
+
>>> combine_filetype_filters({FileType.CAMM}, {FileType.GOPRO, FileType.VIDEO})
|
|
119
|
+
{<FileType.CAMM: 'camm'>}
|
|
120
|
+
|
|
121
|
+
>>> combine_filetype_filters({FileType.GOPRO}, {FileType.GOPRO, FileType.VIDEO})
|
|
122
|
+
{<FileType.GOPRO: 'gopro'>}
|
|
123
|
+
|
|
124
|
+
>>> combine_filetype_filters({FileType.GOPRO}, {FileType.VIDEO})
|
|
125
|
+
{<FileType.GOPRO: 'gopro'>}
|
|
126
|
+
|
|
127
|
+
>>> expected = {FileType.CAMM, FileType.GOPRO}
|
|
128
|
+
>>> combine_filetype_filters({FileType.CAMM, FileType.GOPRO}, {FileType.VIDEO}) == expected
|
|
129
|
+
True
|
|
130
|
+
|
|
131
|
+
>>> expected = {FileType.CAMM, FileType.GOPRO, FileType.BLACKVUE, FileType.VIDEO}
|
|
132
|
+
>>> combine_filetype_filters({FileType.VIDEO}, {FileType.VIDEO}) == expected
|
|
133
|
+
True
|
|
134
|
+
"""
|
|
135
|
+
|
|
124
136
|
if a is None:
|
|
125
137
|
return b
|
|
126
138
|
|
|
@@ -139,81 +151,6 @@ def combine_filetype_filters(
|
|
|
139
151
|
return a.intersection(b)
|
|
140
152
|
|
|
141
153
|
|
|
142
|
-
class UserItem(TypedDict, total=False):
|
|
143
|
-
MAPOrganizationKey: int | str
|
|
144
|
-
# Not in use. Keep here for back-compatibility
|
|
145
|
-
MAPSettingsUsername: str
|
|
146
|
-
MAPSettingsUserKey: str
|
|
147
|
-
user_upload_token: str
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
class _CompassHeading(TypedDict, total=True):
|
|
151
|
-
TrueHeading: float
|
|
152
|
-
MagneticHeading: float
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class _ImageRequired(TypedDict, total=True):
|
|
156
|
-
MAPLatitude: float
|
|
157
|
-
MAPLongitude: float
|
|
158
|
-
MAPCaptureTime: str
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class _Image(_ImageRequired, total=False):
|
|
162
|
-
MAPAltitude: float
|
|
163
|
-
MAPCompassHeading: _CompassHeading
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
class _SequenceOnly(TypedDict, total=False):
|
|
167
|
-
MAPSequenceUUID: str
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class MetaProperties(TypedDict, total=False):
|
|
171
|
-
MAPDeviceMake: str
|
|
172
|
-
MAPDeviceModel: str
|
|
173
|
-
MAPGPSAccuracyMeters: float
|
|
174
|
-
MAPCameraUUID: str
|
|
175
|
-
MAPOrientation: int
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
class ImageDescription(_SequenceOnly, _Image, MetaProperties, total=True):
|
|
179
|
-
# filename is required
|
|
180
|
-
filename: str
|
|
181
|
-
# if None or absent, it will be calculated
|
|
182
|
-
md5sum: str | None
|
|
183
|
-
filetype: Literal["image"]
|
|
184
|
-
filesize: int | None
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class _VideoDescriptionRequired(TypedDict, total=True):
|
|
188
|
-
filename: str
|
|
189
|
-
md5sum: str | None
|
|
190
|
-
filetype: str
|
|
191
|
-
MAPGPSTrack: list[T.Sequence[float | int | None]]
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
class VideoDescription(_VideoDescriptionRequired, total=False):
|
|
195
|
-
MAPDeviceMake: str
|
|
196
|
-
MAPDeviceModel: str
|
|
197
|
-
filesize: int | None
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
class _ErrorDescription(TypedDict, total=False):
|
|
201
|
-
# type and message are required
|
|
202
|
-
type: str
|
|
203
|
-
message: str
|
|
204
|
-
# vars is optional
|
|
205
|
-
vars: dict
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
class _ImageDescriptionErrorRequired(TypedDict, total=True):
|
|
209
|
-
filename: str
|
|
210
|
-
error: _ErrorDescription
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
class ImageDescriptionError(_ImageDescriptionErrorRequired, total=False):
|
|
214
|
-
filetype: str
|
|
215
|
-
|
|
216
|
-
|
|
217
154
|
M = T.TypeVar("M")
|
|
218
155
|
|
|
219
156
|
|
|
@@ -232,511 +169,12 @@ def separate_errors(
|
|
|
232
169
|
return good, bad
|
|
233
170
|
|
|
234
171
|
|
|
235
|
-
def _describe_error_desc(
|
|
236
|
-
exc: Exception, filename: Path, filetype: FileType | None
|
|
237
|
-
) -> ImageDescriptionError:
|
|
238
|
-
err: _ErrorDescription = {
|
|
239
|
-
"type": exc.__class__.__name__,
|
|
240
|
-
"message": str(exc),
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
exc_vars = vars(exc)
|
|
244
|
-
|
|
245
|
-
if exc_vars:
|
|
246
|
-
# handle unserializable exceptions
|
|
247
|
-
try:
|
|
248
|
-
vars_json = json.dumps(exc_vars)
|
|
249
|
-
except Exception:
|
|
250
|
-
vars_json = ""
|
|
251
|
-
if vars_json:
|
|
252
|
-
err["vars"] = json.loads(vars_json)
|
|
253
|
-
|
|
254
|
-
desc: ImageDescriptionError = {
|
|
255
|
-
"error": err,
|
|
256
|
-
"filename": str(filename.resolve()),
|
|
257
|
-
}
|
|
258
|
-
if filetype is not None:
|
|
259
|
-
desc["filetype"] = filetype.value
|
|
260
|
-
|
|
261
|
-
return desc
|
|
262
|
-
|
|
263
|
-
|
|
264
172
|
def describe_error_metadata(
|
|
265
173
|
exc: Exception, filename: Path, filetype: FileType
|
|
266
174
|
) -> ErrorMetadata:
|
|
267
175
|
return ErrorMetadata(filename=filename, filetype=filetype, error=exc)
|
|
268
176
|
|
|
269
177
|
|
|
270
|
-
Description = T.Union[ImageDescription, VideoDescription]
|
|
271
|
-
DescriptionOrError = T.Union[ImageDescription, VideoDescription, ImageDescriptionError]
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
UserItemSchema = {
|
|
275
|
-
"type": "object",
|
|
276
|
-
"properties": {
|
|
277
|
-
"MAPOrganizationKey": {"type": ["integer", "string"]},
|
|
278
|
-
# Not in use. Keep here for back-compatibility
|
|
279
|
-
"MAPSettingsUsername": {"type": "string"},
|
|
280
|
-
"MAPSettingsUserKey": {"type": "string"},
|
|
281
|
-
"user_upload_token": {"type": "string"},
|
|
282
|
-
},
|
|
283
|
-
"required": ["user_upload_token"],
|
|
284
|
-
"additionalProperties": True,
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
ImageDescriptionEXIFSchema = {
|
|
289
|
-
"type": "object",
|
|
290
|
-
"properties": {
|
|
291
|
-
"MAPLatitude": {
|
|
292
|
-
"type": "number",
|
|
293
|
-
"description": "Latitude of the image",
|
|
294
|
-
"minimum": -90,
|
|
295
|
-
"maximum": 90,
|
|
296
|
-
},
|
|
297
|
-
"MAPLongitude": {
|
|
298
|
-
"type": "number",
|
|
299
|
-
"description": "Longitude of the image",
|
|
300
|
-
"minimum": -180,
|
|
301
|
-
"maximum": 180,
|
|
302
|
-
},
|
|
303
|
-
"MAPAltitude": {
|
|
304
|
-
"type": "number",
|
|
305
|
-
"description": "Altitude of the image, in meters",
|
|
306
|
-
},
|
|
307
|
-
"MAPCaptureTime": {
|
|
308
|
-
"type": "string",
|
|
309
|
-
"description": "Capture time of the image",
|
|
310
|
-
"pattern": "[0-9]{4}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]+",
|
|
311
|
-
},
|
|
312
|
-
"MAPCompassHeading": {
|
|
313
|
-
"type": "object",
|
|
314
|
-
"properties": {
|
|
315
|
-
"TrueHeading": {"type": "number"},
|
|
316
|
-
"MagneticHeading": {"type": "number"},
|
|
317
|
-
},
|
|
318
|
-
"required": ["TrueHeading", "MagneticHeading"],
|
|
319
|
-
"additionalProperties": False,
|
|
320
|
-
"description": "Camera angle of the image, in degrees. If null, the angle will be interpolated",
|
|
321
|
-
},
|
|
322
|
-
"MAPSequenceUUID": {
|
|
323
|
-
"type": "string",
|
|
324
|
-
"description": "Arbitrary key for grouping images",
|
|
325
|
-
"pattern": "[a-zA-Z0-9_-]+",
|
|
326
|
-
},
|
|
327
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
328
|
-
"MAPMetaTags": {"type": "object"},
|
|
329
|
-
"MAPDeviceMake": {"type": "string"},
|
|
330
|
-
"MAPDeviceModel": {"type": "string"},
|
|
331
|
-
"MAPGPSAccuracyMeters": {"type": "number"},
|
|
332
|
-
"MAPCameraUUID": {"type": "string"},
|
|
333
|
-
"MAPFilename": {
|
|
334
|
-
"type": "string",
|
|
335
|
-
"description": "The base filename of the image",
|
|
336
|
-
},
|
|
337
|
-
"MAPOrientation": {"type": "integer"},
|
|
338
|
-
},
|
|
339
|
-
"required": [
|
|
340
|
-
"MAPLatitude",
|
|
341
|
-
"MAPLongitude",
|
|
342
|
-
"MAPCaptureTime",
|
|
343
|
-
],
|
|
344
|
-
"additionalProperties": False,
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
VideoDescriptionSchema = {
|
|
348
|
-
"type": "object",
|
|
349
|
-
"properties": {
|
|
350
|
-
"MAPGPSTrack": {
|
|
351
|
-
"type": "array",
|
|
352
|
-
"items": {
|
|
353
|
-
"type": "array",
|
|
354
|
-
"description": "track point",
|
|
355
|
-
"prefixItems": [
|
|
356
|
-
{
|
|
357
|
-
"type": "number",
|
|
358
|
-
"description": "Time offset of the track point, in milliseconds, relative to the beginning of the video",
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
"type": "number",
|
|
362
|
-
"description": "Longitude of the track point",
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
"type": "number",
|
|
366
|
-
"description": "Latitude of the track point",
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
"type": ["number", "null"],
|
|
370
|
-
"description": "Altitude of the track point in meters",
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
"type": ["number", "null"],
|
|
374
|
-
"description": "Camera angle of the track point, in degrees. If null, the angle will be interpolated",
|
|
375
|
-
},
|
|
376
|
-
],
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
"MAPDeviceMake": {
|
|
380
|
-
"type": "string",
|
|
381
|
-
"description": "Device make, e.g. GoPro, BlackVue, Insta360",
|
|
382
|
-
},
|
|
383
|
-
"MAPDeviceModel": {
|
|
384
|
-
"type": "string",
|
|
385
|
-
"description": "Device model, e.g. HERO10 Black, DR900S-1CH, Insta360 Titan",
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
"required": [
|
|
389
|
-
"MAPGPSTrack",
|
|
390
|
-
],
|
|
391
|
-
"additionalProperties": False,
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def merge_schema(*schemas: dict) -> dict:
|
|
396
|
-
for s in schemas:
|
|
397
|
-
assert s.get("type") == "object", "must be all object schemas"
|
|
398
|
-
properties = {}
|
|
399
|
-
all_required = []
|
|
400
|
-
additional_properties = True
|
|
401
|
-
for s in schemas:
|
|
402
|
-
properties.update(s.get("properties", {}))
|
|
403
|
-
all_required += s.get("required", [])
|
|
404
|
-
if "additionalProperties" in s:
|
|
405
|
-
additional_properties = s["additionalProperties"]
|
|
406
|
-
return {
|
|
407
|
-
"type": "object",
|
|
408
|
-
"properties": properties,
|
|
409
|
-
"required": sorted(set(all_required)),
|
|
410
|
-
"additionalProperties": additional_properties,
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
ImageDescriptionFileSchema = merge_schema(
|
|
415
|
-
ImageDescriptionEXIFSchema,
|
|
416
|
-
{
|
|
417
|
-
"type": "object",
|
|
418
|
-
"properties": {
|
|
419
|
-
"filename": {
|
|
420
|
-
"type": "string",
|
|
421
|
-
"description": "Absolute path of the image",
|
|
422
|
-
},
|
|
423
|
-
"md5sum": {
|
|
424
|
-
"type": ["string", "null"],
|
|
425
|
-
"description": "MD5 checksum of the image content. If not provided, the uploader will compute it",
|
|
426
|
-
},
|
|
427
|
-
"filesize": {
|
|
428
|
-
"type": ["number", "null"],
|
|
429
|
-
"description": "File size",
|
|
430
|
-
},
|
|
431
|
-
"filetype": {
|
|
432
|
-
"type": "string",
|
|
433
|
-
"enum": [FileType.IMAGE.value],
|
|
434
|
-
"description": "The image file type",
|
|
435
|
-
},
|
|
436
|
-
},
|
|
437
|
-
"required": [
|
|
438
|
-
"filename",
|
|
439
|
-
"filetype",
|
|
440
|
-
],
|
|
441
|
-
},
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
VideoDescriptionFileSchema = merge_schema(
|
|
445
|
-
VideoDescriptionSchema,
|
|
446
|
-
{
|
|
447
|
-
"type": "object",
|
|
448
|
-
"properties": {
|
|
449
|
-
"filename": {
|
|
450
|
-
"type": "string",
|
|
451
|
-
"description": "Absolute path of the video",
|
|
452
|
-
},
|
|
453
|
-
"md5sum": {
|
|
454
|
-
"type": ["string", "null"],
|
|
455
|
-
"description": "MD5 checksum of the video content. If not provided, the uploader will compute it",
|
|
456
|
-
},
|
|
457
|
-
"filesize": {
|
|
458
|
-
"type": ["number", "null"],
|
|
459
|
-
"description": "File size",
|
|
460
|
-
},
|
|
461
|
-
"filetype": {
|
|
462
|
-
"type": "string",
|
|
463
|
-
"enum": [
|
|
464
|
-
FileType.CAMM.value,
|
|
465
|
-
FileType.GOPRO.value,
|
|
466
|
-
FileType.BLACKVUE.value,
|
|
467
|
-
FileType.VIDEO.value,
|
|
468
|
-
],
|
|
469
|
-
"description": "The video file type",
|
|
470
|
-
},
|
|
471
|
-
},
|
|
472
|
-
"required": [
|
|
473
|
-
"filename",
|
|
474
|
-
"filetype",
|
|
475
|
-
],
|
|
476
|
-
},
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
ImageVideoDescriptionFileSchema = {
|
|
481
|
-
"oneOf": [VideoDescriptionFileSchema, ImageDescriptionFileSchema]
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
def validate_image_desc(desc: T.Any) -> None:
|
|
486
|
-
try:
|
|
487
|
-
jsonschema.validate(instance=desc, schema=ImageDescriptionFileSchema)
|
|
488
|
-
except jsonschema.ValidationError as ex:
|
|
489
|
-
# do not use str(ex) which is more verbose
|
|
490
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
491
|
-
try:
|
|
492
|
-
map_capture_time_to_datetime(desc["MAPCaptureTime"])
|
|
493
|
-
except ValueError as ex:
|
|
494
|
-
raise exceptions.MapillaryMetadataValidationError(str(ex)) from ex
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def validate_video_desc(desc: T.Any) -> None:
|
|
498
|
-
try:
|
|
499
|
-
jsonschema.validate(instance=desc, schema=VideoDescriptionFileSchema)
|
|
500
|
-
except jsonschema.ValidationError as ex:
|
|
501
|
-
# do not use str(ex) which is more verbose
|
|
502
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def datetime_to_map_capture_time(time: datetime.datetime | int | float) -> str:
|
|
506
|
-
if isinstance(time, (float, int)):
|
|
507
|
-
dt = datetime.datetime.fromtimestamp(time, datetime.timezone.utc)
|
|
508
|
-
# otherwise it will be assumed to be in local time
|
|
509
|
-
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
510
|
-
else:
|
|
511
|
-
# otherwise it will be assumed to be in local time
|
|
512
|
-
dt = time.astimezone(datetime.timezone.utc)
|
|
513
|
-
return datetime.datetime.strftime(dt, "%Y_%m_%d_%H_%M_%S_%f")[:-3]
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
def map_capture_time_to_datetime(time: str) -> datetime.datetime:
|
|
517
|
-
dt = datetime.datetime.strptime(time, "%Y_%m_%d_%H_%M_%S_%f")
|
|
518
|
-
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
519
|
-
return dt
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
@T.overload
|
|
523
|
-
def as_desc(metadata: ImageMetadata) -> ImageDescription: ...
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
@T.overload
|
|
527
|
-
def as_desc(metadata: ErrorMetadata) -> ImageDescriptionError: ...
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
@T.overload
|
|
531
|
-
def as_desc(metadata: VideoMetadata) -> VideoDescription: ...
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
def as_desc(metadata):
|
|
535
|
-
if isinstance(metadata, ErrorMetadata):
|
|
536
|
-
return _describe_error_desc(
|
|
537
|
-
metadata.error, metadata.filename, metadata.filetype
|
|
538
|
-
)
|
|
539
|
-
elif isinstance(metadata, VideoMetadata):
|
|
540
|
-
return _as_video_desc(metadata)
|
|
541
|
-
else:
|
|
542
|
-
assert isinstance(metadata, ImageMetadata)
|
|
543
|
-
return _as_image_desc(metadata)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def _as_video_desc(metadata: VideoMetadata) -> VideoDescription:
|
|
547
|
-
desc: VideoDescription = {
|
|
548
|
-
"filename": str(metadata.filename.resolve()),
|
|
549
|
-
"md5sum": metadata.md5sum,
|
|
550
|
-
"filetype": metadata.filetype.value,
|
|
551
|
-
"filesize": metadata.filesize,
|
|
552
|
-
"MAPGPSTrack": [_encode_point(p) for p in metadata.points],
|
|
553
|
-
}
|
|
554
|
-
if metadata.make:
|
|
555
|
-
desc["MAPDeviceMake"] = metadata.make
|
|
556
|
-
if metadata.model:
|
|
557
|
-
desc["MAPDeviceModel"] = metadata.model
|
|
558
|
-
return desc
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
def _as_image_desc(metadata: ImageMetadata) -> ImageDescription:
|
|
562
|
-
desc: ImageDescription = {
|
|
563
|
-
"filename": str(metadata.filename.resolve()),
|
|
564
|
-
"md5sum": metadata.md5sum,
|
|
565
|
-
"filesize": metadata.filesize,
|
|
566
|
-
"filetype": FileType.IMAGE.value,
|
|
567
|
-
"MAPLatitude": round(metadata.lat, _COORDINATES_PRECISION),
|
|
568
|
-
"MAPLongitude": round(metadata.lon, _COORDINATES_PRECISION),
|
|
569
|
-
"MAPCaptureTime": datetime_to_map_capture_time(metadata.time),
|
|
570
|
-
}
|
|
571
|
-
if metadata.alt is not None:
|
|
572
|
-
desc["MAPAltitude"] = round(metadata.alt, _ALTITUDE_PRECISION)
|
|
573
|
-
if metadata.angle is not None:
|
|
574
|
-
desc["MAPCompassHeading"] = {
|
|
575
|
-
"TrueHeading": round(metadata.angle, _ANGLE_PRECISION),
|
|
576
|
-
"MagneticHeading": round(metadata.angle, _ANGLE_PRECISION),
|
|
577
|
-
}
|
|
578
|
-
fields = dataclasses.fields(metadata)
|
|
579
|
-
for field in fields:
|
|
580
|
-
if field.name.startswith("MAP"):
|
|
581
|
-
value = getattr(metadata, field.name)
|
|
582
|
-
if value is not None:
|
|
583
|
-
# ignore error: TypedDict key must be a string literal;
|
|
584
|
-
# expected one of ("MAPMetaTags", "MAPDeviceMake", "MAPDeviceModel", "MAPGPSAccuracyMeters", "MAPCameraUUID", ...)
|
|
585
|
-
desc[field.name] = value # type: ignore
|
|
586
|
-
return desc
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
@T.overload
|
|
590
|
-
def from_desc(metadata: ImageDescription) -> ImageMetadata: ...
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
@T.overload
|
|
594
|
-
def from_desc(metadata: VideoDescription) -> VideoMetadata: ...
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def from_desc(desc):
|
|
598
|
-
assert "error" not in desc
|
|
599
|
-
if desc["filetype"] == FileType.IMAGE.value:
|
|
600
|
-
return _from_image_desc(desc)
|
|
601
|
-
else:
|
|
602
|
-
return _from_video_desc(desc)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
def _from_image_desc(desc) -> ImageMetadata:
|
|
606
|
-
kwargs: dict = {}
|
|
607
|
-
for k, v in desc.items():
|
|
608
|
-
if k not in [
|
|
609
|
-
"filename",
|
|
610
|
-
"md5sum",
|
|
611
|
-
"filesize",
|
|
612
|
-
"filetype",
|
|
613
|
-
"MAPLatitude",
|
|
614
|
-
"MAPLongitude",
|
|
615
|
-
"MAPAltitude",
|
|
616
|
-
"MAPCaptureTime",
|
|
617
|
-
"MAPCompassHeading",
|
|
618
|
-
]:
|
|
619
|
-
kwargs[k] = v
|
|
620
|
-
|
|
621
|
-
return ImageMetadata(
|
|
622
|
-
filename=Path(desc["filename"]),
|
|
623
|
-
md5sum=desc.get("md5sum"),
|
|
624
|
-
filesize=desc.get("filesize"),
|
|
625
|
-
lat=desc["MAPLatitude"],
|
|
626
|
-
lon=desc["MAPLongitude"],
|
|
627
|
-
alt=desc.get("MAPAltitude"),
|
|
628
|
-
time=geo.as_unix_time(map_capture_time_to_datetime(desc["MAPCaptureTime"])),
|
|
629
|
-
angle=desc.get("MAPCompassHeading", {}).get("TrueHeading"),
|
|
630
|
-
width=None,
|
|
631
|
-
height=None,
|
|
632
|
-
**kwargs,
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
def _encode_point(p: geo.Point) -> T.Sequence[float | int | None]:
|
|
637
|
-
entry = [
|
|
638
|
-
int(p.time * 1000),
|
|
639
|
-
round(p.lon, _COORDINATES_PRECISION),
|
|
640
|
-
round(p.lat, _COORDINATES_PRECISION),
|
|
641
|
-
round(p.alt, _ALTITUDE_PRECISION) if p.alt is not None else None,
|
|
642
|
-
round(p.angle, _ANGLE_PRECISION) if p.angle is not None else None,
|
|
643
|
-
]
|
|
644
|
-
return entry
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def _decode_point(entry: T.Sequence[T.Any]) -> geo.Point:
|
|
648
|
-
time_ms, lon, lat, alt, angle = entry
|
|
649
|
-
return geo.Point(time=time_ms / 1000, lon=lon, lat=lat, alt=alt, angle=angle)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
def _from_video_desc(desc: VideoDescription) -> VideoMetadata:
|
|
653
|
-
return VideoMetadata(
|
|
654
|
-
filename=Path(desc["filename"]),
|
|
655
|
-
md5sum=desc["md5sum"],
|
|
656
|
-
filesize=desc["filesize"],
|
|
657
|
-
filetype=FileType(desc["filetype"]),
|
|
658
|
-
points=[_decode_point(entry) for entry in desc["MAPGPSTrack"]],
|
|
659
|
-
make=desc.get("MAPDeviceMake"),
|
|
660
|
-
model=desc.get("MAPDeviceModel"),
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
def validate_and_fail_desc(desc: DescriptionOrError) -> DescriptionOrError:
|
|
665
|
-
if "error" in desc:
|
|
666
|
-
return desc
|
|
667
|
-
|
|
668
|
-
filetype = desc.get("filetype")
|
|
669
|
-
try:
|
|
670
|
-
if filetype == FileType.IMAGE.value:
|
|
671
|
-
validate_image_desc(desc)
|
|
672
|
-
else:
|
|
673
|
-
validate_video_desc(desc)
|
|
674
|
-
except exceptions.MapillaryMetadataValidationError as ex:
|
|
675
|
-
return _describe_error_desc(
|
|
676
|
-
ex,
|
|
677
|
-
Path(desc["filename"]),
|
|
678
|
-
filetype=FileType(filetype) if filetype else None,
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
if not os.path.isfile(desc["filename"]):
|
|
682
|
-
return _describe_error_desc(
|
|
683
|
-
exceptions.MapillaryMetadataValidationError(
|
|
684
|
-
f"No such file {desc['filename']}"
|
|
685
|
-
),
|
|
686
|
-
Path(desc["filename"]),
|
|
687
|
-
filetype=FileType(filetype) if filetype else None,
|
|
688
|
-
)
|
|
689
|
-
|
|
690
|
-
return desc
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
# Same as validate_and_fail_desc but for the metadata dataclass
|
|
694
|
-
def validate_and_fail_metadata(metadata: MetadataOrError) -> MetadataOrError:
|
|
695
|
-
if isinstance(metadata, ErrorMetadata):
|
|
696
|
-
return metadata
|
|
697
|
-
|
|
698
|
-
if isinstance(metadata, ImageMetadata):
|
|
699
|
-
filetype = FileType.IMAGE
|
|
700
|
-
validate = validate_image_desc
|
|
701
|
-
else:
|
|
702
|
-
assert isinstance(metadata, VideoMetadata)
|
|
703
|
-
filetype = metadata.filetype
|
|
704
|
-
validate = validate_video_desc
|
|
705
|
-
|
|
706
|
-
try:
|
|
707
|
-
validate(as_desc(metadata))
|
|
708
|
-
except exceptions.MapillaryMetadataValidationError as ex:
|
|
709
|
-
# rethrow because the original error is too verbose
|
|
710
|
-
return describe_error_metadata(
|
|
711
|
-
ex,
|
|
712
|
-
metadata.filename,
|
|
713
|
-
filetype=filetype,
|
|
714
|
-
)
|
|
715
|
-
|
|
716
|
-
if not metadata.filename.is_file():
|
|
717
|
-
return describe_error_metadata(
|
|
718
|
-
exceptions.MapillaryMetadataValidationError(
|
|
719
|
-
f"No such file {metadata.filename}"
|
|
720
|
-
),
|
|
721
|
-
metadata.filename,
|
|
722
|
-
filetype=filetype,
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
return metadata
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
def desc_file_to_exif(
|
|
729
|
-
desc: ImageDescription,
|
|
730
|
-
) -> ImageDescription:
|
|
731
|
-
not_needed = ["MAPSequenceUUID"]
|
|
732
|
-
removed = {
|
|
733
|
-
key: value
|
|
734
|
-
for key, value in desc.items()
|
|
735
|
-
if key.startswith("MAP") and key not in not_needed
|
|
736
|
-
}
|
|
737
|
-
return T.cast(ImageDescription, removed)
|
|
738
|
-
|
|
739
|
-
|
|
740
178
|
def group_and_sort_images(
|
|
741
179
|
metadatas: T.Iterable[ImageMetadata],
|
|
742
180
|
) -> dict[str, list[ImageMetadata]]:
|
|
@@ -768,7 +206,3 @@ def update_sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
|
|
|
768
206
|
assert isinstance(metadata.md5sum, str), "md5sum should be calculated"
|
|
769
207
|
md5.update(metadata.md5sum.encode("utf-8"))
|
|
770
208
|
return md5.hexdigest()
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if __name__ == "__main__":
|
|
774
|
-
print(json.dumps(ImageVideoDescriptionFileSchema, indent=4))
|