mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a1__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 (64) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +235 -14
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +425 -177
  7. mapillary_tools/commands/__main__.py +11 -4
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +19 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +28 -12
  15. mapillary_tools/constants.py +46 -4
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +158 -53
  18. mapillary_tools/exiftool_read.py +19 -5
  19. mapillary_tools/exiftool_read_video.py +12 -1
  20. mapillary_tools/exiftool_runner.py +77 -0
  21. mapillary_tools/geo.py +148 -107
  22. mapillary_tools/geotag/factory.py +298 -0
  23. mapillary_tools/geotag/geotag_from_generic.py +152 -11
  24. mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
  25. mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
  26. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
  27. mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
  28. mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
  29. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
  30. mapillary_tools/geotag/geotag_images_from_video.py +46 -46
  31. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
  32. mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
  33. mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
  34. mapillary_tools/geotag/options.py +159 -0
  35. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
  36. mapillary_tools/history.py +3 -11
  37. mapillary_tools/mp4/io_utils.py +0 -1
  38. mapillary_tools/mp4/mp4_sample_parser.py +11 -3
  39. mapillary_tools/mp4/simple_mp4_parser.py +0 -10
  40. mapillary_tools/process_geotag_properties.py +151 -386
  41. mapillary_tools/process_sequence_properties.py +554 -202
  42. mapillary_tools/sample_video.py +8 -15
  43. mapillary_tools/telemetry.py +24 -12
  44. mapillary_tools/types.py +80 -22
  45. mapillary_tools/upload.py +316 -298
  46. mapillary_tools/upload_api_v4.py +55 -122
  47. mapillary_tools/uploader.py +396 -254
  48. mapillary_tools/utils.py +26 -0
  49. mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
  50. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
  51. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
  52. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
  53. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
  54. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
  55. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
  56. mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
  57. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
  58. mapillary_tools/geotag/utils.py +0 -26
  59. mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
  60. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  61. /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
  62. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import datetime
2
4
  import logging
3
5
  import os
@@ -11,7 +13,6 @@ from . import constants, exceptions, ffmpeg as ffmpeglib, geo, types, utils
11
13
  from .exif_write import ExifEdit
12
14
  from .geotag import geotag_videos_from_video
13
15
  from .mp4 import mp4_sample_parser
14
- from .process_geotag_properties import GeotagSource
15
16
 
16
17
  LOG = logging.getLogger(__name__)
17
18
 
@@ -46,7 +47,6 @@ def sample_video(
46
47
  video_import_path: Path,
47
48
  import_path: Path,
48
49
  # None if called from the sample_video command
49
- geotag_source: T.Optional[GeotagSource] = None,
50
50
  skip_subfolders=False,
51
51
  video_sample_distance=constants.VIDEO_SAMPLE_DISTANCE,
52
52
  video_sample_interval=constants.VIDEO_SAMPLE_INTERVAL,
@@ -86,16 +86,6 @@ def sample_video(
86
86
  elif sample_dir.is_file():
87
87
  os.remove(sample_dir)
88
88
 
89
- if geotag_source is None:
90
- geotag_source = "exif"
91
-
92
- # If it is not exif, then we use the legacy interval-based sample and geotag them in "process" for backward compatibility
93
- if geotag_source not in ["exif"]:
94
- if 0 <= video_sample_distance:
95
- raise exceptions.MapillaryBadParameterError(
96
- f'Geotagging from "{geotag_source}" works with the legacy interval-based sampling only. To switch back, rerun the command with "--video_sample_distance -1 --video_sample_interval 2"'
97
- )
98
-
99
89
  for video_path in video_list:
100
90
  # need to resolve video_path because video_dir might be absolute
101
91
  sample_dir = Path(import_path).joinpath(
@@ -299,9 +289,12 @@ def _sample_single_video_by_distance(
299
289
  )
300
290
 
301
291
  LOG.info("Extracting video metdata")
302
- video_metadata = geotag_videos_from_video.GeotagVideosFromVideo.geotag_video(
303
- video_path
304
- )
292
+
293
+ video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
294
+ [video_path]
295
+ ).to_description()
296
+ assert len(video_metadatas) == 1, "expect 1 video metadata"
297
+ video_metadata = video_metadatas[0]
305
298
  if isinstance(video_metadata, types.ErrorMetadata):
306
299
  LOG.warning(str(video_metadata.error))
307
300
  return
@@ -12,16 +12,8 @@ class GPSFix(Enum):
12
12
  FIX_3D = 3
13
13
 
14
14
 
15
- @dataclasses.dataclass
16
- class GPSPoint(Point):
17
- epoch_time: T.Optional[float]
18
- fix: T.Optional[GPSFix]
19
- precision: T.Optional[float]
20
- ground_speed: T.Optional[float]
21
-
22
-
23
15
  @dataclasses.dataclass(order=True)
24
- class TelemetryMeasurement:
16
+ class TimestampedMeasurement:
25
17
  """Base class for all telemetry measurements.
26
18
 
27
19
  All telemetry measurements must have a timestamp in seconds.
@@ -32,8 +24,28 @@ class TelemetryMeasurement:
32
24
  time: float
33
25
 
34
26
 
27
+ @dataclasses.dataclass
28
+ class GPSPoint(TimestampedMeasurement, Point):
29
+ epoch_time: T.Optional[float]
30
+ fix: T.Optional[GPSFix]
31
+ precision: T.Optional[float]
32
+ ground_speed: T.Optional[float]
33
+
34
+
35
+ @dataclasses.dataclass
36
+ class CAMMGPSPoint(TimestampedMeasurement, Point):
37
+ time_gps_epoch: float
38
+ gps_fix_type: int
39
+ horizontal_accuracy: float
40
+ vertical_accuracy: float
41
+ velocity_east: float
42
+ velocity_north: float
43
+ velocity_up: float
44
+ speed_accuracy: float
45
+
46
+
35
47
  @dataclasses.dataclass(order=True)
36
- class GyroscopeData(TelemetryMeasurement):
48
+ class GyroscopeData(TimestampedMeasurement):
37
49
  """Gyroscope signal in radians/seconds around XYZ axes of the camera."""
38
50
 
39
51
  x: float
@@ -42,7 +54,7 @@ class GyroscopeData(TelemetryMeasurement):
42
54
 
43
55
 
44
56
  @dataclasses.dataclass(order=True)
45
- class AccelerationData(TelemetryMeasurement):
57
+ class AccelerationData(TimestampedMeasurement):
46
58
  """Accelerometer reading in meters/second^2 along XYZ axes of the camera."""
47
59
 
48
60
  x: float
@@ -51,7 +63,7 @@ class AccelerationData(TelemetryMeasurement):
51
63
 
52
64
 
53
65
  @dataclasses.dataclass(order=True)
54
- class MagnetometerData(TelemetryMeasurement):
66
+ class MagnetometerData(TimestampedMeasurement):
55
67
  """Ambient magnetic field."""
56
68
 
57
69
  x: float
mapillary_tools/types.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
4
  import datetime
3
5
  import enum
@@ -31,19 +33,28 @@ _ANGLE_PRECISION = 3
31
33
 
32
34
 
33
35
  class FileType(enum.Enum):
36
+ IMAGE = "image"
37
+ ZIP = "zip"
38
+ # VIDEO is a superset of all NATIVE_VIDEO_FILETYPES below.
39
+ # It also contains the videos that external geotag source (e.g. exiftool) supports
40
+ VIDEO = "video"
34
41
  BLACKVUE = "blackvue"
35
42
  CAMM = "camm"
36
43
  GOPRO = "gopro"
37
- IMAGE = "image"
38
- VIDEO = "video"
39
- ZIP = "zip"
44
+
45
+
46
+ NATIVE_VIDEO_FILETYPES = {
47
+ FileType.BLACKVUE,
48
+ FileType.CAMM,
49
+ FileType.GOPRO,
50
+ }
40
51
 
41
52
 
42
53
  @dataclasses.dataclass
43
54
  class ImageMetadata(geo.Point):
44
55
  filename: Path
45
56
  # if None or absent, it will be calculated
46
- md5sum: T.Optional[str]
57
+ md5sum: T.Optional[str] = None
47
58
  # filetype: is always FileType.IMAGE
48
59
  width: T.Optional[int] = None
49
60
  height: T.Optional[int] = None
@@ -55,7 +66,6 @@ class ImageMetadata(geo.Point):
55
66
  MAPOrientation: T.Optional[int] = None
56
67
  # deprecated since v0.10.0; keep here for compatibility
57
68
  MAPMetaTags: T.Optional[T.Dict] = None
58
- # deprecated since v0.10.0; keep here for compatibility
59
69
  MAPFilename: T.Optional[str] = None
60
70
  filesize: T.Optional[int] = None
61
71
 
@@ -78,9 +88,9 @@ class ImageMetadata(geo.Point):
78
88
  class VideoMetadata:
79
89
  filename: Path
80
90
  # if None or absent, it will be calculated
81
- md5sum: T.Optional[str]
82
91
  filetype: FileType
83
92
  points: T.Sequence[geo.Point]
93
+ md5sum: T.Optional[str] = None
84
94
  make: T.Optional[str] = None
85
95
  model: T.Optional[str] = None
86
96
  filesize: T.Optional[int] = None
@@ -94,7 +104,7 @@ class VideoMetadata:
94
104
  @dataclasses.dataclass
95
105
  class ErrorMetadata:
96
106
  filename: Path
97
- filetype: T.Optional[FileType]
107
+ filetype: FileType
98
108
  error: Exception
99
109
 
100
110
 
@@ -104,6 +114,35 @@ Metadata = T.Union[ImageMetadata, VideoMetadata]
104
114
  MetadataOrError = T.Union[Metadata, ErrorMetadata]
105
115
 
106
116
 
117
+ # Assume {GOPRO, VIDEO} are the NATIVE_VIDEO_FILETYPES:
118
+ # a | b = result
119
+ # {CAMM} | {GOPRO} = {}
120
+ # {CAMM} | {GOPRO, VIDEO} = {CAMM}
121
+ # {GOPRO} | {GOPRO, VIDEO} = {GOPRO}
122
+ # {GOPRO} | {VIDEO} = {GOPRO}
123
+ # {CAMM, GOPRO} | {VIDEO} = {CAMM, GOPRO}
124
+ # {VIDEO} | {VIDEO} = {CAMM, GOPRO, VIDEO}
125
+ def combine_filetype_filters(
126
+ a: set[FileType] | None, b: set[FileType] | None
127
+ ) -> set[FileType] | None:
128
+ if a is None:
129
+ return b
130
+
131
+ if b is None:
132
+ return a
133
+
134
+ # VIDEO is a superset of NATIVE_VIDEO_FILETYPES,
135
+ # so we add NATIVE_VIDEO_FILETYPES to each set for intersection later
136
+
137
+ if FileType.VIDEO in a:
138
+ a = a | NATIVE_VIDEO_FILETYPES
139
+
140
+ if FileType.VIDEO in b:
141
+ b = b | NATIVE_VIDEO_FILETYPES
142
+
143
+ return a.intersection(b)
144
+
145
+
107
146
  class UserItem(TypedDict, total=False):
108
147
  MAPOrganizationKey: T.Union[int, str]
109
148
  # Not in use. Keep here for back-compatibility
@@ -180,6 +219,24 @@ class ImageDescriptionError(_ImageDescriptionErrorRequired, total=False):
180
219
  filetype: str
181
220
 
182
221
 
222
+ M = T.TypeVar("M")
223
+
224
+
225
+ def separate_errors(
226
+ metadatas: T.Iterable[M | ErrorMetadata],
227
+ ) -> tuple[list[M], list[ErrorMetadata]]:
228
+ good: list[M] = []
229
+ bad: list[ErrorMetadata] = []
230
+
231
+ for metadata in metadatas:
232
+ if isinstance(metadata, ErrorMetadata):
233
+ bad.append(metadata)
234
+ else:
235
+ good.append(metadata)
236
+
237
+ return good, bad
238
+
239
+
183
240
  def _describe_error_desc(
184
241
  exc: Exception, filename: Path, filetype: T.Optional[FileType]
185
242
  ) -> ImageDescriptionError:
@@ -210,7 +267,7 @@ def _describe_error_desc(
210
267
 
211
268
 
212
269
  def describe_error_metadata(
213
- exc: Exception, filename: Path, filetype: T.Optional[FileType]
270
+ exc: Exception, filename: Path, filetype: FileType
214
271
  ) -> ErrorMetadata:
215
272
  return ErrorMetadata(filename=filename, filetype=filetype, error=exc)
216
273
 
@@ -278,7 +335,6 @@ ImageDescriptionEXIFSchema = {
278
335
  "MAPDeviceModel": {"type": "string"},
279
336
  "MAPGPSAccuracyMeters": {"type": "number"},
280
337
  "MAPCameraUUID": {"type": "string"},
281
- # deprecated since v0.10.0; keep here for compatibility
282
338
  "MAPFilename": {
283
339
  "type": "string",
284
340
  "description": "The base filename of the image",
@@ -436,11 +492,11 @@ def validate_image_desc(desc: T.Any) -> None:
436
492
  jsonschema.validate(instance=desc, schema=ImageDescriptionFileSchema)
437
493
  except jsonschema.ValidationError as ex:
438
494
  # do not use str(ex) which is more verbose
439
- raise exceptions.MapillaryMetadataValidationError(ex.message)
495
+ raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
440
496
  try:
441
497
  map_capture_time_to_datetime(desc["MAPCaptureTime"])
442
498
  except ValueError as ex:
443
- raise exceptions.MapillaryMetadataValidationError(str(ex))
499
+ raise exceptions.MapillaryMetadataValidationError(str(ex)) from ex
444
500
 
445
501
 
446
502
  def validate_video_desc(desc: T.Any) -> None:
@@ -448,12 +504,12 @@ def validate_video_desc(desc: T.Any) -> None:
448
504
  jsonschema.validate(instance=desc, schema=VideoDescriptionFileSchema)
449
505
  except jsonschema.ValidationError as ex:
450
506
  # do not use str(ex) which is more verbose
451
- raise exceptions.MapillaryMetadataValidationError(ex.message)
507
+ raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
452
508
 
453
509
 
454
510
  def datetime_to_map_capture_time(time: T.Union[datetime.datetime, int, float]) -> str:
455
511
  if isinstance(time, (float, int)):
456
- dt = datetime.datetime.utcfromtimestamp(time)
512
+ dt = datetime.datetime.fromtimestamp(time, datetime.timezone.utc)
457
513
  # otherwise it will be assumed to be in local time
458
514
  dt = dt.replace(tzinfo=datetime.timezone.utc)
459
515
  else:
@@ -644,15 +700,16 @@ def validate_and_fail_metadata(metadata: MetadataOrError) -> MetadataOrError:
644
700
  if isinstance(metadata, ErrorMetadata):
645
701
  return metadata
646
702
 
647
- filetype: T.Optional[FileType] = None
703
+ if isinstance(metadata, ImageMetadata):
704
+ filetype = FileType.IMAGE
705
+ validate = validate_image_desc
706
+ else:
707
+ assert isinstance(metadata, VideoMetadata)
708
+ filetype = metadata.filetype
709
+ validate = validate_video_desc
710
+
648
711
  try:
649
- if isinstance(metadata, ImageMetadata):
650
- filetype = FileType.IMAGE
651
- validate_image_desc(as_desc(metadata))
652
- else:
653
- assert isinstance(metadata, VideoMetadata)
654
- filetype = metadata.filetype
655
- validate_video_desc(as_desc(metadata))
712
+ validate(as_desc(metadata))
656
713
  except exceptions.MapillaryMetadataValidationError as ex:
657
714
  # rethrow because the original error is too verbose
658
715
  return describe_error_metadata(
@@ -709,9 +766,10 @@ def group_and_sort_images(
709
766
  return sorted_sequences_by_uuid
710
767
 
711
768
 
712
- def sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
769
+ def update_sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
713
770
  md5 = hashlib.md5()
714
771
  for metadata in sequence:
772
+ metadata.update_md5sum()
715
773
  assert isinstance(metadata.md5sum, str), "md5sum should be calculated"
716
774
  md5.update(metadata.md5sum.encode("utf-8"))
717
775
  return md5.hexdigest()