mapillary-tools 0.12.1__py3-none-any.whl → 0.13.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 (59) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +94 -4
  3. mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
  4. mapillary_tools/camm/camm_parser.py +561 -0
  5. mapillary_tools/commands/__init__.py +0 -1
  6. mapillary_tools/commands/__main__.py +0 -6
  7. mapillary_tools/commands/process.py +0 -50
  8. mapillary_tools/commands/upload.py +1 -26
  9. mapillary_tools/constants.py +2 -2
  10. mapillary_tools/exiftool_read_video.py +13 -11
  11. mapillary_tools/ffmpeg.py +2 -2
  12. mapillary_tools/geo.py +0 -54
  13. mapillary_tools/geotag/blackvue_parser.py +4 -4
  14. mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
  15. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
  16. mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
  17. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
  18. mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
  19. mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
  20. mapillary_tools/geotag/gpmf_parser.py +346 -83
  21. mapillary_tools/mp4/__init__.py +0 -0
  22. mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
  23. mapillary_tools/mp4/mp4_sample_parser.py +322 -0
  24. mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
  25. mapillary_tools/process_geotag_properties.py +25 -19
  26. mapillary_tools/process_sequence_properties.py +6 -6
  27. mapillary_tools/sample_video.py +17 -16
  28. mapillary_tools/telemetry.py +71 -0
  29. mapillary_tools/types.py +18 -0
  30. mapillary_tools/upload.py +74 -233
  31. mapillary_tools/upload_api_v4.py +8 -9
  32. mapillary_tools/utils.py +9 -16
  33. mapillary_tools/video_data_extraction/cli_options.py +0 -1
  34. mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
  35. mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
  36. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
  37. mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
  38. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
  39. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
  40. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
  41. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
  42. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
  43. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
  44. mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
  45. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
  46. mapillary_tools-0.13.1.dist-info/RECORD +75 -0
  47. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
  48. mapillary_tools/commands/upload_blackvue.py +0 -33
  49. mapillary_tools/commands/upload_camm.py +0 -33
  50. mapillary_tools/commands/upload_zip.py +0 -33
  51. mapillary_tools/geotag/camm_parser.py +0 -306
  52. mapillary_tools/geotag/mp4_sample_parser.py +0 -426
  53. mapillary_tools/process_import_meta_properties.py +0 -76
  54. mapillary_tools-0.12.1.dist-info/RECORD +0 -77
  55. /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
  56. /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
  57. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
  58. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
  59. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- VERSION = "0.12.1"
1
+ VERSION = "0.13.1"
mapillary_tools/api_v4.py CHANGED
@@ -1,8 +1,12 @@
1
+ import logging
1
2
  import os
3
+ import ssl
2
4
  import typing as T
3
5
 
4
6
  import requests
7
+ from requests.adapters import HTTPAdapter
5
8
 
9
+ LOG = logging.getLogger(__name__)
6
10
  MAPILLARY_CLIENT_TOKEN = os.getenv(
7
11
  "MAPILLARY_CLIENT_TOKEN", "MLY|5675152195860640|6b02c72e6e3c801e5603ab0495623282"
8
12
  )
@@ -10,10 +14,96 @@ MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
10
14
  "MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
11
15
  )
12
16
  REQUESTS_TIMEOUT = 60 # 1 minutes
17
+ USE_SYSTEM_CERTS: bool = False
18
+
19
+
20
+ class HTTPSystemCertsAdapter(HTTPAdapter):
21
+ """
22
+ This adapter uses the system's certificate store instead of the certifi module.
23
+
24
+ The implementation is based on the project https://pypi.org/project/pip-system-certs/,
25
+ which has a system-wide effect.
26
+ """
27
+
28
+ def init_poolmanager(self, *args, **kwargs):
29
+ ssl_context = ssl.create_default_context()
30
+ ssl_context.load_default_certs()
31
+ kwargs["ssl_context"] = ssl_context
32
+
33
+ super().init_poolmanager(*args, **kwargs)
34
+
35
+ def cert_verify(self, *args, **kwargs):
36
+ super().cert_verify(*args, **kwargs)
37
+
38
+ # By default Python requests uses the ca_certs from the certifi module
39
+ # But we want to use the certificate store instead.
40
+ # By clearing the ca_certs variable we force it to fall back on that behaviour (handled in urllib3)
41
+ if "conn" in kwargs:
42
+ conn = kwargs["conn"]
43
+ else:
44
+ conn = args[0]
45
+
46
+ conn.ca_certs = None
47
+
48
+
49
+ def request_post(
50
+ url: str,
51
+ data: T.Optional[T.Any] = None,
52
+ json: T.Optional[dict] = None,
53
+ **kwargs,
54
+ ) -> requests.Response:
55
+ global USE_SYSTEM_CERTS
56
+
57
+ if USE_SYSTEM_CERTS:
58
+ with requests.Session() as session:
59
+ session.mount("https://", HTTPSystemCertsAdapter())
60
+ return session.post(url, data=data, json=json, **kwargs)
61
+
62
+ else:
63
+ try:
64
+ return requests.post(url, data=data, json=json, **kwargs)
65
+ except requests.exceptions.SSLError as ex:
66
+ if "SSLCertVerificationError" not in str(ex):
67
+ raise ex
68
+ USE_SYSTEM_CERTS = True
69
+ # HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
70
+ LOG.warning(
71
+ "SSL error occurred, falling back to system SSL certificates: %s", ex
72
+ )
73
+ with requests.Session() as session:
74
+ session.mount("https://", HTTPSystemCertsAdapter())
75
+ return session.post(url, data=data, json=json, **kwargs)
76
+
77
+
78
+ def request_get(
79
+ url: str,
80
+ params: T.Optional[dict] = None,
81
+ **kwargs,
82
+ ) -> requests.Response:
83
+ global USE_SYSTEM_CERTS
84
+
85
+ if USE_SYSTEM_CERTS:
86
+ with requests.Session() as session:
87
+ session.mount("https://", HTTPSystemCertsAdapter())
88
+ return session.get(url, params=params, **kwargs)
89
+ else:
90
+ try:
91
+ return requests.get(url, params=params, **kwargs)
92
+ except requests.exceptions.SSLError as ex:
93
+ if "SSLCertVerificationError" not in str(ex):
94
+ raise ex
95
+ USE_SYSTEM_CERTS = True
96
+ # HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
97
+ LOG.warning(
98
+ "SSL error occurred, falling back to system SSL certificates: %s", ex
99
+ )
100
+ with requests.Session() as session:
101
+ session.mount("https://", HTTPSystemCertsAdapter())
102
+ return session.get(url, params=params, **kwargs)
13
103
 
14
104
 
15
105
  def get_upload_token(email: str, password: str) -> requests.Response:
16
- resp = requests.post(
106
+ resp = request_post(
17
107
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
18
108
  params={"access_token": MAPILLARY_CLIENT_TOKEN},
19
109
  json={"email": email, "password": password, "locale": "en_US"},
@@ -26,7 +116,7 @@ def get_upload_token(email: str, password: str) -> requests.Response:
26
116
  def fetch_organization(
27
117
  user_access_token: str, organization_id: T.Union[int, str]
28
118
  ) -> requests.Response:
29
- resp = requests.get(
119
+ resp = request_get(
30
120
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
31
121
  params={
32
122
  "fields": ",".join(["slug", "description", "name"]),
@@ -45,8 +135,8 @@ ActionType = T.Literal[
45
135
  ]
46
136
 
47
137
 
48
- def logging(action_type: ActionType, properties: T.Dict) -> requests.Response:
49
- resp = requests.post(
138
+ def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
139
+ resp = request_post(
50
140
  f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
51
141
  json={
52
142
  "action_type": action_type,
@@ -2,60 +2,46 @@ import io
2
2
  import typing as T
3
3
 
4
4
  from .. import geo, types
5
-
6
- from . import (
7
- camm_parser,
5
+ from ..mp4 import (
8
6
  construct_mp4_parser as cparser,
9
7
  mp4_sample_parser as sample_parser,
10
8
  simple_mp4_builder as builder,
11
9
  )
12
- from .simple_mp4_builder import BoxDict
13
10
 
11
+ from . import camm_parser
14
12
 
15
- def build_camm_sample(point: geo.Point) -> bytes:
16
- return camm_parser.CAMMSampleData.build(
17
- {
18
- "type": camm_parser.CAMMType.MIN_GPS.value,
19
- "data": [
20
- point.lat,
21
- point.lon,
22
- -1.0 if point.alt is None else point.alt,
23
- ],
24
- }
25
- )
26
13
 
14
+ def _build_camm_sample(measurement: camm_parser.TelemetryMeasurement) -> bytes:
15
+ for sample_entry_cls in camm_parser.SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.values():
16
+ if sample_entry_cls.serializable(measurement):
17
+ return sample_entry_cls.serialize(measurement)
18
+ raise ValueError(f"Unsupported measurement type {type(measurement)}")
27
19
 
28
- def _create_edit_list(
20
+
21
+ def _create_edit_list_from_points(
29
22
  point_segments: T.Sequence[T.Sequence[geo.Point]],
30
23
  movie_timescale: int,
31
24
  media_timescale: int,
32
- ) -> BoxDict:
25
+ ) -> builder.BoxDict:
33
26
  entries: T.List[T.Dict] = []
34
27
 
35
- for idx, points in enumerate(point_segments):
36
- if not points:
37
- entries = [
38
- {
39
- "media_time": 0,
40
- "segment_duration": 0,
41
- "media_rate_integer": 1,
42
- "media_rate_fraction": 0,
43
- }
44
- ]
45
- break
28
+ non_empty_point_segments = [points for points in point_segments if points]
46
29
 
47
- assert (
48
- 0 <= points[0].time
49
- ), f"expect non-negative point time but got {points[0]}"
50
- assert (
51
- points[0].time <= points[-1].time
52
- ), f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
30
+ for idx, points in enumerate(non_empty_point_segments):
31
+ assert 0 <= points[0].time, (
32
+ f"expect non-negative point time but got {points[0]}"
33
+ )
34
+ assert points[0].time <= points[-1].time, (
35
+ f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
36
+ )
53
37
 
54
38
  if idx == 0:
55
39
  if 0 < points[0].time:
56
40
  segment_duration = int(points[0].time * movie_timescale)
41
+ # put an empty edit list entry to skip the initial gap
57
42
  entries.append(
58
43
  {
44
+ # If this field is set to –1, it is an empty edit
59
45
  "media_time": -1,
60
46
  "segment_duration": segment_duration,
61
47
  "media_rate_integer": 1,
@@ -63,7 +49,6 @@ def _create_edit_list(
63
49
  }
64
50
  )
65
51
  else:
66
- assert point_segments[-1][-1].time <= points[0].time
67
52
  media_time = int(points[0].time * media_timescale)
68
53
  segment_duration = int((points[-1].time - points[0].time) * movie_timescale)
69
54
  entries.append(
@@ -83,19 +68,34 @@ def _create_edit_list(
83
68
  }
84
69
 
85
70
 
86
- def convert_points_to_raw_samples(
87
- points: T.Sequence[geo.Point], timescale: int
71
+ def _multiplex(
72
+ points: T.Sequence[geo.Point],
73
+ measurements: T.Optional[T.List[camm_parser.TelemetryMeasurement]] = None,
74
+ ) -> T.List[camm_parser.TelemetryMeasurement]:
75
+ mutiplexed: T.List[camm_parser.TelemetryMeasurement] = [
76
+ *points,
77
+ *(measurements or []),
78
+ ]
79
+ mutiplexed.sort(key=lambda m: m.time)
80
+
81
+ return mutiplexed
82
+
83
+
84
+ def convert_telemetry_to_raw_samples(
85
+ measurements: T.Sequence[camm_parser.TelemetryMeasurement],
86
+ timescale: int,
88
87
  ) -> T.Generator[sample_parser.RawSample, None, None]:
89
- for idx, point in enumerate(points):
90
- camm_sample_data = build_camm_sample(point)
88
+ for idx, measurement in enumerate(measurements):
89
+ camm_sample_data = _build_camm_sample(measurement)
91
90
 
92
- if idx + 1 < len(points):
93
- timedelta = int((points[idx + 1].time - point.time) * timescale)
91
+ if idx + 1 < len(measurements):
92
+ timedelta = int((measurements[idx + 1].time - measurement.time) * timescale)
94
93
  else:
95
94
  timedelta = 0
96
- assert (
97
- 0 <= timedelta <= builder.UINT32_MAX
98
- ), f"expected timedelta {timedelta} between {points[idx]} and {points[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
95
+
96
+ assert 0 <= timedelta <= builder.UINT32_MAX, (
97
+ f"expected timedelta {timedelta} between {measurements[idx]} and {measurements[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
98
+ )
99
99
 
100
100
  yield sample_parser.RawSample(
101
101
  # will update later
@@ -114,7 +114,9 @@ _STBLChildrenBuilderConstruct = cparser.Box32ConstructBuilder(
114
114
  )
115
115
 
116
116
 
117
- def _create_camm_stbl(raw_samples: T.Iterable[sample_parser.RawSample]) -> BoxDict:
117
+ def _create_camm_stbl(
118
+ raw_samples: T.Iterable[sample_parser.RawSample],
119
+ ) -> builder.BoxDict:
118
120
  descriptions = [
119
121
  {
120
122
  "format": b"camm",
@@ -135,10 +137,10 @@ def _create_camm_stbl(raw_samples: T.Iterable[sample_parser.RawSample]) -> BoxDi
135
137
  def create_camm_trak(
136
138
  raw_samples: T.Sequence[sample_parser.RawSample],
137
139
  media_timescale: int,
138
- ) -> BoxDict:
140
+ ) -> builder.BoxDict:
139
141
  stbl = _create_camm_stbl(raw_samples)
140
142
 
141
- hdlr: BoxDict = {
143
+ hdlr: builder.BoxDict = {
142
144
  "type": b"hdlr",
143
145
  "data": {
144
146
  "handler_type": b"camm",
@@ -150,7 +152,7 @@ def create_camm_trak(
150
152
  assert media_timescale <= builder.UINT64_MAX
151
153
 
152
154
  # Media Header Box
153
- mdhd: BoxDict = {
155
+ mdhd: builder.BoxDict = {
154
156
  "type": b"mdhd",
155
157
  "data": {
156
158
  # use 64-bit version
@@ -166,7 +168,7 @@ def create_camm_trak(
166
168
  },
167
169
  }
168
170
 
169
- dinf: BoxDict = {
171
+ dinf: builder.BoxDict = {
170
172
  "type": b"dinf",
171
173
  "data": [
172
174
  # self reference dref box
@@ -187,7 +189,7 @@ def create_camm_trak(
187
189
  ],
188
190
  }
189
191
 
190
- minf: BoxDict = {
192
+ minf: builder.BoxDict = {
191
193
  "type": b"minf",
192
194
  "data": [
193
195
  dinf,
@@ -195,7 +197,7 @@ def create_camm_trak(
195
197
  ],
196
198
  }
197
199
 
198
- tkhd: BoxDict = {
200
+ tkhd: builder.BoxDict = {
199
201
  "type": b"tkhd",
200
202
  "data": {
201
203
  # use 32-bit version of the box
@@ -213,7 +215,7 @@ def create_camm_trak(
213
215
  },
214
216
  }
215
217
 
216
- mdia: BoxDict = {
218
+ mdia: builder.BoxDict = {
217
219
  "type": b"mdia",
218
220
  "data": [
219
221
  mdhd,
@@ -231,23 +233,31 @@ def create_camm_trak(
231
233
  }
232
234
 
233
235
 
234
- def camm_sample_generator2(video_metadata: types.VideoMetadata):
236
+ def camm_sample_generator2(
237
+ video_metadata: types.VideoMetadata,
238
+ telemetry_measurements: T.Optional[T.List[camm_parser.TelemetryMeasurement]] = None,
239
+ ):
235
240
  def _f(
236
241
  fp: T.BinaryIO,
237
- moov_children: T.List[BoxDict],
242
+ moov_children: T.List[builder.BoxDict],
238
243
  ) -> T.Generator[io.IOBase, None, None]:
239
244
  movie_timescale = builder.find_movie_timescale(moov_children)
240
245
  # make sure the precision of timedeltas not lower than 0.001 (1ms)
241
246
  media_timescale = max(1000, movie_timescale)
247
+
248
+ # points with negative time are skipped
249
+ # TODO: interpolate first point at time == 0
250
+ # TODO: measurements with negative times should be skipped too
251
+ points = [point for point in video_metadata.points if point.time >= 0]
252
+
253
+ measurements = _multiplex(points, telemetry_measurements)
242
254
  camm_samples = list(
243
- convert_points_to_raw_samples(video_metadata.points, media_timescale)
255
+ convert_telemetry_to_raw_samples(measurements, media_timescale)
244
256
  )
245
257
  camm_trak = create_camm_trak(camm_samples, media_timescale)
246
- elst = _create_edit_list(
247
- [video_metadata.points], movie_timescale, media_timescale
248
- )
258
+ elst = _create_edit_list_from_points([points], movie_timescale, media_timescale)
249
259
  if T.cast(T.Dict, elst["data"])["entries"]:
250
- T.cast(T.List[BoxDict], camm_trak["data"]).append(
260
+ T.cast(T.List[builder.BoxDict], camm_trak["data"]).append(
251
261
  {
252
262
  "type": b"edts",
253
263
  "data": [elst],
@@ -255,7 +265,7 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
255
265
  )
256
266
  moov_children.append(camm_trak)
257
267
 
258
- udta_data: T.List[BoxDict] = []
268
+ udta_data: T.List[builder.BoxDict] = []
259
269
  if video_metadata.make:
260
270
  udta_data.append(
261
271
  {
@@ -279,6 +289,8 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
279
289
  )
280
290
 
281
291
  # if yield, the moov_children will not be modified
282
- return (io.BytesIO(build_camm_sample(point)) for point in video_metadata.points)
292
+ return (
293
+ io.BytesIO(_build_camm_sample(measurement)) for measurement in measurements
294
+ )
283
295
 
284
296
  return _f