mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.1__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 (49) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -262
  3. mapillary_tools/authenticate.py +54 -46
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/commands/__main__.py +15 -16
  6. mapillary_tools/commands/upload.py +33 -4
  7. mapillary_tools/config.py +38 -17
  8. mapillary_tools/constants.py +127 -43
  9. mapillary_tools/exceptions.py +4 -0
  10. mapillary_tools/exif_read.py +2 -1
  11. mapillary_tools/exif_write.py +3 -1
  12. mapillary_tools/exiftool_read_video.py +52 -15
  13. mapillary_tools/exiftool_runner.py +4 -24
  14. mapillary_tools/ffmpeg.py +406 -232
  15. mapillary_tools/geo.py +16 -0
  16. mapillary_tools/geotag/__init__.py +0 -0
  17. mapillary_tools/geotag/base.py +8 -4
  18. mapillary_tools/geotag/factory.py +106 -89
  19. mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
  20. mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
  21. mapillary_tools/geotag/geotag_images_from_video.py +35 -0
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
  24. mapillary_tools/geotag/options.py +25 -3
  25. mapillary_tools/geotag/utils.py +9 -12
  26. mapillary_tools/geotag/video_extractors/base.py +1 -1
  27. mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
  28. mapillary_tools/geotag/video_extractors/gpx.py +61 -70
  29. mapillary_tools/geotag/video_extractors/native.py +34 -31
  30. mapillary_tools/history.py +128 -8
  31. mapillary_tools/http.py +211 -0
  32. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  33. mapillary_tools/process_geotag_properties.py +47 -35
  34. mapillary_tools/process_sequence_properties.py +340 -325
  35. mapillary_tools/sample_video.py +8 -8
  36. mapillary_tools/serializer/description.py +587 -0
  37. mapillary_tools/serializer/gpx.py +132 -0
  38. mapillary_tools/types.py +44 -610
  39. mapillary_tools/upload.py +327 -352
  40. mapillary_tools/upload_api_v4.py +125 -72
  41. mapillary_tools/uploader.py +797 -216
  42. mapillary_tools/utils.py +57 -5
  43. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
  44. mapillary_tools-0.14.1.dist-info/RECORD +76 -0
  45. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
  46. mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
  47. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  48. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  49. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.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 jsonschema
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
- # Assume {GOPRO, VIDEO} are the NATIVE_VIDEO_FILETYPES:
114
- # a | b = result
115
- # {CAMM} | {GOPRO} = {}
116
- # {CAMM} | {GOPRO, VIDEO} = {CAMM}
117
- # {GOPRO} | {GOPRO, VIDEO} = {GOPRO}
118
- # {GOPRO} | {VIDEO} = {GOPRO}
119
- # {CAMM, GOPRO} | {VIDEO} = {CAMM, GOPRO}
120
- # {VIDEO} | {VIDEO} = {CAMM, GOPRO, VIDEO}
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))