mapillary-tools 0.13.3__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 +106 -7
  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 +2 -0
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +18 -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 +311 -261
  46. mapillary_tools/upload_api_v4.py +55 -95
  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.3.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.3.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.3.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.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
1
5
  import json
2
6
  import logging
3
- import pathlib
4
7
  import re
5
8
  import typing as T
6
9
 
7
10
  import pynmea2
8
11
 
9
- from .. import geo
10
- from ..mp4 import simple_mp4_parser as sparser
12
+ from . import geo
13
+ from .mp4 import simple_mp4_parser as sparser
11
14
 
12
15
 
13
16
  LOG = logging.getLogger(__name__)
@@ -26,31 +29,45 @@ NMEA_LINE_REGEX = re.compile(
26
29
  )
27
30
 
28
31
 
29
- def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]:
30
- for line_bytes in gps_data.splitlines():
31
- match = NMEA_LINE_REGEX.match(line_bytes)
32
- if match is None:
33
- continue
34
- nmea_line_bytes = match.group(2)
35
- if nmea_line_bytes.startswith(b"$GPGGA"):
36
- try:
37
- nmea_line = nmea_line_bytes.decode("utf8")
38
- except UnicodeDecodeError:
39
- continue
40
- try:
41
- nmea = pynmea2.parse(nmea_line)
42
- except pynmea2.nmea.ParseError:
43
- continue
44
- if not nmea.is_valid:
45
- continue
46
- epoch_ms = int(match.group(1))
47
- yield geo.Point(
48
- time=epoch_ms,
49
- lat=nmea.latitude,
50
- lon=nmea.longitude,
51
- alt=nmea.altitude,
52
- angle=None,
53
- )
32
+ @dataclasses.dataclass
33
+ class BlackVueInfo:
34
+ # None and [] are equivalent here. Use None as default because:
35
+ # ValueError: mutable default <class 'list'> for field gps is not allowed: use default_factory
36
+ gps: list[geo.Point] | None = None
37
+ make: str = "BlackVue"
38
+ model: str = ""
39
+
40
+
41
+ def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None:
42
+ try:
43
+ gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "])
44
+ except sparser.ParsingError:
45
+ gps_data = None
46
+
47
+ if gps_data is None:
48
+ return None
49
+
50
+ points = list(_parse_gps_box(gps_data))
51
+ points.sort(key=lambda p: p.time)
52
+
53
+ if points:
54
+ first_point_time = points[0].time
55
+ for p in points:
56
+ p.time = (p.time - first_point_time) / 1000
57
+
58
+ # Camera model
59
+ try:
60
+ cprt_bytes = sparser.parse_mp4_data_first(fp, [b"free", b"cprt"])
61
+ except sparser.ParsingError:
62
+ cprt_bytes = None
63
+ model = ""
64
+
65
+ if cprt_bytes is None:
66
+ model = ""
67
+ else:
68
+ model = _extract_camera_model_from_cprt(cprt_bytes)
69
+
70
+ return BlackVueInfo(model=model, gps=points)
54
71
 
55
72
 
56
73
  def extract_camera_model(fp: T.BinaryIO) -> str:
@@ -62,6 +79,10 @@ def extract_camera_model(fp: T.BinaryIO) -> str:
62
79
  if cprt_bytes is None:
63
80
  return ""
64
81
 
82
+ return _extract_camera_model_from_cprt(cprt_bytes)
83
+
84
+
85
+ def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
65
86
  # examples: b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}\x00'
66
87
  # b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;\x00'
67
88
  cprt_bytes = cprt_bytes.strip().strip(b"\x00")
@@ -90,29 +111,28 @@ def extract_camera_model(fp: T.BinaryIO) -> str:
90
111
  return ""
91
112
 
92
113
 
93
- def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]:
94
- gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "])
95
- if gps_data is None:
96
- return None
97
-
98
- points = list(_parse_gps_box(gps_data))
99
- if not points:
100
- return points
101
-
102
- points.sort(key=lambda p: p.time)
103
-
104
- first_point_time = points[0].time
105
- for p in points:
106
- p.time = (p.time - first_point_time) / 1000
107
-
108
- return points
109
-
110
-
111
- def parse_gps_points(path: pathlib.Path) -> T.List[geo.Point]:
112
- with path.open("rb") as fp:
113
- points = extract_points(fp)
114
-
115
- if points is None:
116
- return []
117
-
118
- return points
114
+ def _parse_gps_box(gps_data: bytes) -> T.Generator[geo.Point, None, None]:
115
+ for line_bytes in gps_data.splitlines():
116
+ match = NMEA_LINE_REGEX.match(line_bytes)
117
+ if match is None:
118
+ continue
119
+ nmea_line_bytes = match.group(2)
120
+ if nmea_line_bytes.startswith(b"$GPGGA"):
121
+ try:
122
+ nmea_line = nmea_line_bytes.decode("utf8")
123
+ except UnicodeDecodeError:
124
+ continue
125
+ try:
126
+ nmea = pynmea2.parse(nmea_line)
127
+ except pynmea2.nmea.ParseError:
128
+ continue
129
+ if not nmea.is_valid:
130
+ continue
131
+ epoch_ms = int(match.group(1))
132
+ yield geo.Point(
133
+ time=epoch_ms,
134
+ lat=nmea.latitude,
135
+ lon=nmea.longitude,
136
+ alt=nmea.altitude,
137
+ angle=None,
138
+ )
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
4
  import typing as T
3
5
 
4
- from .. import geo, telemetry, types
6
+ from .. import geo
5
7
  from ..mp4 import (
6
8
  construct_mp4_parser as cparser,
7
9
  mp4_sample_parser as sample_parser,
@@ -11,84 +13,35 @@ from ..mp4 import (
11
13
  from . import camm_parser
12
14
 
13
15
 
14
- TelemetryMeasurement = T.Union[
15
- geo.Point,
16
- telemetry.TelemetryMeasurement,
17
- ]
16
+ def _build_camm_sample(measurement: camm_parser.TelemetryMeasurement) -> bytes:
17
+ if camm_parser.GoProGPSSampleEntry.serializable(measurement):
18
+ return camm_parser.GoProGPSSampleEntry.serialize(measurement)
18
19
 
20
+ for sample_entry_cls in camm_parser.SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.values():
21
+ if sample_entry_cls.serializable(measurement):
22
+ return sample_entry_cls.serialize(measurement)
19
23
 
20
- def _build_camm_sample(measurement: TelemetryMeasurement) -> bytes:
21
- if isinstance(measurement, geo.Point):
22
- return camm_parser.CAMMSampleData.build(
23
- {
24
- "type": camm_parser.CAMMType.MIN_GPS.value,
25
- "data": [
26
- measurement.lat,
27
- measurement.lon,
28
- -1.0 if measurement.alt is None else measurement.alt,
29
- ],
30
- }
31
- )
32
- elif isinstance(measurement, telemetry.AccelerationData):
33
- # Accelerometer reading in meters/second^2 along XYZ axes of the camera.
34
- return camm_parser.CAMMSampleData.build(
35
- {
36
- "type": camm_parser.CAMMType.ACCELERATION.value,
37
- "data": [
38
- measurement.x,
39
- measurement.y,
40
- measurement.z,
41
- ],
42
- }
43
- )
44
- elif isinstance(measurement, telemetry.GyroscopeData):
45
- # Gyroscope signal in radians/seconds around XYZ axes of the camera. Rotation is positive in the counterclockwise direction.
46
- return camm_parser.CAMMSampleData.build(
47
- {
48
- "type": camm_parser.CAMMType.GYRO.value,
49
- "data": [
50
- measurement.x,
51
- measurement.y,
52
- measurement.z,
53
- ],
54
- }
55
- )
56
- elif isinstance(measurement, telemetry.MagnetometerData):
57
- # Ambient magnetic field.
58
- return camm_parser.CAMMSampleData.build(
59
- {
60
- "type": camm_parser.CAMMType.MAGNETIC_FIELD.value,
61
- "data": [
62
- measurement.x,
63
- measurement.y,
64
- measurement.z,
65
- ],
66
- }
67
- )
68
- else:
69
- raise ValueError(f"unexpected measurement type {type(measurement)}")
24
+ raise ValueError(f"Unsupported measurement type {type(measurement)}")
70
25
 
71
26
 
72
27
  def _create_edit_list_from_points(
73
- point_segments: T.Sequence[T.Sequence[geo.Point]],
28
+ tracks: T.Sequence[T.Sequence[geo.Point]],
74
29
  movie_timescale: int,
75
30
  media_timescale: int,
76
31
  ) -> builder.BoxDict:
77
- entries: T.List[T.Dict] = []
32
+ entries: list[dict] = []
78
33
 
79
- non_empty_point_segments = [points for points in point_segments if points]
34
+ non_empty_tracks = [track for track in tracks if track]
80
35
 
81
- for idx, points in enumerate(non_empty_point_segments):
82
- assert 0 <= points[0].time, (
83
- f"expect non-negative point time but got {points[0]}"
84
- )
85
- assert points[0].time <= points[-1].time, (
86
- f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
36
+ for idx, track in enumerate(non_empty_tracks):
37
+ assert 0 <= track[0].time, f"expect non-negative point time but got {track[0]}"
38
+ assert track[0].time <= track[-1].time, (
39
+ f"expect points to be sorted but got first point {track[0]} and last point {track[-1]}"
87
40
  )
88
41
 
89
42
  if idx == 0:
90
- if 0 < points[0].time:
91
- segment_duration = int(points[0].time * movie_timescale)
43
+ if 0 < track[0].time:
44
+ segment_duration = int(track[0].time * movie_timescale)
92
45
  # put an empty edit list entry to skip the initial gap
93
46
  entries.append(
94
47
  {
@@ -100,8 +53,8 @@ def _create_edit_list_from_points(
100
53
  }
101
54
  )
102
55
  else:
103
- media_time = int(points[0].time * media_timescale)
104
- segment_duration = int((points[-1].time - points[0].time) * movie_timescale)
56
+ media_time = int(track[0].time * media_timescale)
57
+ segment_duration = int((track[-1].time - track[0].time) * movie_timescale)
105
58
  entries.append(
106
59
  {
107
60
  "media_time": media_time,
@@ -119,18 +72,8 @@ def _create_edit_list_from_points(
119
72
  }
120
73
 
121
74
 
122
- def _multiplex(
123
- points: T.Sequence[geo.Point],
124
- measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
125
- ) -> T.List[TelemetryMeasurement]:
126
- mutiplexed: T.List[TelemetryMeasurement] = [*points, *(measurements or [])]
127
- mutiplexed.sort(key=lambda m: m.time)
128
-
129
- return mutiplexed
130
-
131
-
132
75
  def convert_telemetry_to_raw_samples(
133
- measurements: T.Sequence[TelemetryMeasurement],
76
+ measurements: T.Sequence[camm_parser.TelemetryMeasurement],
134
77
  timescale: int,
135
78
  ) -> T.Generator[sample_parser.RawSample, None, None]:
136
79
  for idx, measurement in enumerate(measurements):
@@ -281,29 +224,44 @@ def create_camm_trak(
281
224
  }
282
225
 
283
226
 
284
- def camm_sample_generator2(
285
- video_metadata: types.VideoMetadata,
286
- telemetry_measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
287
- ):
227
+ def camm_sample_generator2(camm_info: camm_parser.CAMMInfo):
288
228
  def _f(
289
229
  fp: T.BinaryIO,
290
- moov_children: T.List[builder.BoxDict],
230
+ moov_children: list[builder.BoxDict],
291
231
  ) -> T.Generator[io.IOBase, None, None]:
292
232
  movie_timescale = builder.find_movie_timescale(moov_children)
293
- # make sure the precision of timedeltas not lower than 0.001 (1ms)
233
+ # Make sure the precision of timedeltas not lower than 0.001 (1ms)
294
234
  media_timescale = max(1000, movie_timescale)
295
235
 
296
- # points with negative time are skipped
297
- # TODO: interpolate first point at time == 0
298
- # TODO: measurements with negative times should be skipped too
299
- points = [point for point in video_metadata.points if point.time >= 0]
300
-
301
- measurements = _multiplex(points, telemetry_measurements)
236
+ # Multiplex points for creating elst
237
+ track: list[geo.Point] = [
238
+ *(camm_info.gps or []),
239
+ *(camm_info.mini_gps or []),
240
+ ]
241
+ track.sort(key=lambda p: p.time)
242
+ if track and track[0].time < 0:
243
+ track = [p for p in track if p.time >= 0]
244
+ elst = _create_edit_list_from_points([track], movie_timescale, media_timescale)
245
+
246
+ # Multiplex telemetry measurements
247
+ measurements: list[camm_parser.TelemetryMeasurement] = [
248
+ *(camm_info.gps or []),
249
+ *(camm_info.mini_gps or []),
250
+ *(camm_info.accl or []),
251
+ *(camm_info.gyro or []),
252
+ *(camm_info.magn or []),
253
+ ]
254
+ measurements.sort(key=lambda m: m.time)
255
+ if measurements and measurements[0].time < 0:
256
+ measurements = [m for m in measurements if m.time >= 0]
257
+
258
+ # Serialize the telemetry measurements into MP4 samples
302
259
  camm_samples = list(
303
260
  convert_telemetry_to_raw_samples(measurements, media_timescale)
304
261
  )
262
+
305
263
  camm_trak = create_camm_trak(camm_samples, media_timescale)
306
- elst = _create_edit_list_from_points([points], movie_timescale, media_timescale)
264
+
307
265
  if T.cast(T.Dict, elst["data"])["entries"]:
308
266
  T.cast(T.List[builder.BoxDict], camm_trak["data"]).append(
309
267
  {
@@ -313,19 +271,19 @@ def camm_sample_generator2(
313
271
  )
314
272
  moov_children.append(camm_trak)
315
273
 
316
- udta_data: T.List[builder.BoxDict] = []
317
- if video_metadata.make:
274
+ udta_data: list[builder.BoxDict] = []
275
+ if camm_info.make:
318
276
  udta_data.append(
319
277
  {
320
278
  "type": b"@mak",
321
- "data": video_metadata.make.encode("utf-8"),
279
+ "data": camm_info.make.encode("utf-8"),
322
280
  }
323
281
  )
324
- if video_metadata.model:
282
+ if camm_info.model:
325
283
  udta_data.append(
326
284
  {
327
285
  "type": b"@mod",
328
- "data": video_metadata.model.encode("utf-8"),
286
+ "data": camm_info.model.encode("utf-8"),
329
287
  }
330
288
  )
331
289
  if udta_data: