mapillary-tools 0.13.0__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.
@@ -1 +1 @@
1
- VERSION = "0.13.0"
1
+ VERSION = "0.13.1"
@@ -1,7 +1,7 @@
1
1
  import io
2
2
  import typing as T
3
3
 
4
- from .. import geo, telemetry, types
4
+ from .. import geo, types
5
5
  from ..mp4 import (
6
6
  construct_mp4_parser as cparser,
7
7
  mp4_sample_parser as sample_parser,
@@ -11,62 +11,11 @@ from ..mp4 import (
11
11
  from . import camm_parser
12
12
 
13
13
 
14
- TelemetryMeasurement = T.Union[
15
- geo.Point,
16
- telemetry.TelemetryMeasurement,
17
- ]
18
-
19
-
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)}")
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)}")
70
19
 
71
20
 
72
21
  def _create_edit_list_from_points(
@@ -121,16 +70,19 @@ def _create_edit_list_from_points(
121
70
 
122
71
  def _multiplex(
123
72
  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 [])]
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
+ ]
127
79
  mutiplexed.sort(key=lambda m: m.time)
128
80
 
129
81
  return mutiplexed
130
82
 
131
83
 
132
84
  def convert_telemetry_to_raw_samples(
133
- measurements: T.Sequence[TelemetryMeasurement],
85
+ measurements: T.Sequence[camm_parser.TelemetryMeasurement],
134
86
  timescale: int,
135
87
  ) -> T.Generator[sample_parser.RawSample, None, None]:
136
88
  for idx, measurement in enumerate(measurements):
@@ -283,7 +235,7 @@ def create_camm_trak(
283
235
 
284
236
  def camm_sample_generator2(
285
237
  video_metadata: types.VideoMetadata,
286
- telemetry_measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
238
+ telemetry_measurements: T.Optional[T.List[camm_parser.TelemetryMeasurement]] = None,
287
239
  ):
288
240
  def _f(
289
241
  fp: T.BinaryIO,
@@ -1,5 +1,6 @@
1
1
  # pyre-ignore-all-errors[5, 11, 16, 21, 24, 58]
2
2
 
3
+ import abc
3
4
  import dataclasses
4
5
  import io
5
6
  import logging
@@ -19,9 +20,7 @@ LOG = logging.getLogger(__name__)
19
20
 
20
21
  TelemetryMeasurement = T.Union[
21
22
  geo.Point,
22
- telemetry.AccelerationData,
23
- telemetry.GyroscopeData,
24
- telemetry.MagnetometerData,
23
+ telemetry.TelemetryMeasurement,
25
24
  ]
26
25
 
27
26
 
@@ -36,100 +35,320 @@ class CAMMType(Enum):
36
35
  GPS = 6
37
36
  MAGNETIC_FIELD = 7
38
37
 
38
+ # Mapillary extensions are offset by 1024
39
+ # GoPro GPS is not compatible with CAMMType.GPS,
40
+ # so we use a new type to represent it
41
+ MLY_GOPRO_GPS = 1024 + 6
42
+
39
43
 
40
44
  # All fields are little-endian
41
45
  Float = C.Float32l
42
46
  Double = C.Float64l
43
47
 
44
- _SWITCH: T.Dict[int, C.Struct] = {
45
- # angle_axis
46
- CAMMType.ANGLE_AXIS.value: Float[3],
47
- CAMMType.EXPOSURE_TIME.value: C.Struct(
48
- "pixel_exposure_time" / C.Int32sl,
49
- "rolling_shutter_skew_time" / C.Int32sl,
50
- ),
51
- # gyro
52
- CAMMType.GYRO.value: Float[3],
53
- # acceleration
54
- CAMMType.ACCELERATION.value: Float[3],
55
- # position
56
- CAMMType.POSITION.value: Float[3],
57
- # lat, lon, alt
58
- CAMMType.MIN_GPS.value: Double[3],
59
- CAMMType.GPS.value: C.Struct(
60
- "time_gps_epoch" / Double,
61
- "gps_fix_type" / C.Int32sl,
62
- "latitude" / Double,
63
- "longitude" / Double,
64
- "altitude" / Float,
65
- "horizontal_accuracy" / Float,
66
- "vertical_accuracy" / Float,
67
- "velocity_east" / Float,
68
- "velocity_north" / Float,
69
- "velocity_up" / Float,
70
- "speed_accuracy" / Float,
71
- ),
72
- # magnetic_field
73
- CAMMType.MAGNETIC_FIELD.value: Float[3],
74
- }
75
48
 
76
- CAMMSampleData = C.Struct(
77
- C.Padding(2),
78
- "type" / C.Int16ul,
79
- "data"
80
- / C.Switch(
81
- C.this.type,
82
- _SWITCH,
83
- ),
84
- )
49
+ TTelemetry = T.TypeVar("TTelemetry", bound=TelemetryMeasurement)
85
50
 
86
51
 
87
- def _parse_telemetry_from_sample(
88
- fp: T.BinaryIO, sample: Sample
89
- ) -> T.Optional[TelemetryMeasurement]:
90
- fp.seek(sample.raw_sample.offset, io.SEEK_SET)
91
- data = fp.read(sample.raw_sample.size)
92
- box = CAMMSampleData.parse(data)
93
- if box.type == CAMMType.MIN_GPS.value:
52
+ class CAMMSampleEntry(abc.ABC, T.Generic[TTelemetry]):
53
+ camm_type: CAMMType
54
+
55
+ construct: C.Struct
56
+
57
+ telemetry_cls: T.Type[TTelemetry]
58
+
59
+ @classmethod
60
+ def serializable(cls, data: T.Any, throw: bool = False) -> bool:
61
+ # Use "is" for exact type match, instead of isinstance
62
+ if type(data) is cls.telemetry_cls:
63
+ return True
64
+
65
+ if throw:
66
+ raise TypeError(
67
+ f"{cls} can not serialize {type(data)}: expect {cls.telemetry_cls}"
68
+ )
69
+ return False
70
+
71
+ @classmethod
72
+ @abc.abstractmethod
73
+ def serialize(cls, data: TTelemetry) -> bytes:
74
+ raise NotImplementedError
75
+
76
+ @classmethod
77
+ @abc.abstractmethod
78
+ def deserialize(cls, sample: Sample, data: T.Any) -> TTelemetry:
79
+ raise NotImplementedError
80
+
81
+
82
+ class MinGPSSampleEntry(CAMMSampleEntry):
83
+ camm_type = CAMMType.MIN_GPS
84
+
85
+ construct = Double[3] # type: ignore
86
+
87
+ telemetry_cls = geo.Point
88
+
89
+ @classmethod
90
+ def deserialize(cls, sample: Sample, data: T.Any) -> geo.Point:
94
91
  return geo.Point(
95
92
  time=sample.exact_time,
96
- lat=box.data[0],
97
- lon=box.data[1],
98
- alt=box.data[2],
93
+ lat=data[0],
94
+ lon=data[1],
95
+ alt=data[2],
99
96
  angle=None,
100
97
  )
101
- elif box.type == CAMMType.GPS.value:
102
- # Not using box.data.time_gps_epoch as the point timestamp
103
- # because it is from another clock
104
- return geo.Point(
98
+
99
+ @classmethod
100
+ def serialize(cls, data: geo.Point) -> bytes:
101
+ cls.serializable(data, throw=True)
102
+
103
+ return CAMMSampleData.build(
104
+ {
105
+ "type": cls.camm_type.value,
106
+ "data": [
107
+ data.lat,
108
+ data.lon,
109
+ -1.0 if data.alt is None else data.alt,
110
+ ],
111
+ }
112
+ )
113
+
114
+
115
+ class GPSSampleEntry(CAMMSampleEntry):
116
+ camm_type: CAMMType = CAMMType.GPS
117
+
118
+ construct = C.Struct(
119
+ "time_gps_epoch" / Double, # type: ignore
120
+ "gps_fix_type" / C.Int32sl, # type: ignore
121
+ "latitude" / Double, # type: ignore
122
+ "longitude" / Double, # type: ignore
123
+ "altitude" / Float, # type: ignore
124
+ "horizontal_accuracy" / Float, # type: ignore
125
+ "vertical_accuracy" / Float, # type: ignore
126
+ "velocity_east" / Float, # type: ignore
127
+ "velocity_north" / Float, # type: ignore
128
+ "velocity_up" / Float, # type: ignore
129
+ "speed_accuracy" / Float, # type: ignore
130
+ )
131
+
132
+ telemetry_cls = telemetry.CAMMGPSPoint
133
+
134
+ @classmethod
135
+ def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.CAMMGPSPoint:
136
+ return telemetry.CAMMGPSPoint(
105
137
  time=sample.exact_time,
106
- lat=box.data.latitude,
107
- lon=box.data.longitude,
108
- alt=box.data.altitude,
138
+ lat=data.latitude,
139
+ lon=data.longitude,
140
+ alt=data.altitude,
109
141
  angle=None,
142
+ time_gps_epoch=data.time_gps_epoch,
143
+ gps_fix_type=data.gps_fix_type,
144
+ horizontal_accuracy=data.horizontal_accuracy,
145
+ vertical_accuracy=data.vertical_accuracy,
146
+ velocity_east=data.velocity_east,
147
+ velocity_north=data.velocity_north,
148
+ velocity_up=data.velocity_up,
149
+ speed_accuracy=data.speed_accuracy,
150
+ )
151
+
152
+ @classmethod
153
+ def serialize(cls, data: telemetry.CAMMGPSPoint) -> bytes:
154
+ cls.serializable(data, throw=True)
155
+
156
+ return CAMMSampleData.build(
157
+ {
158
+ "type": cls.camm_type.value,
159
+ "data": {
160
+ "time_gps_epoch": data.time_gps_epoch,
161
+ "gps_fix_type": data.gps_fix_type,
162
+ "latitude": data.lat,
163
+ "longitude": data.lon,
164
+ "altitude": -1.0 if data.alt is None else data.alt,
165
+ "horizontal_accuracy": data.horizontal_accuracy,
166
+ "vertical_accuracy": data.vertical_accuracy,
167
+ "velocity_east": data.velocity_east,
168
+ "velocity_north": data.velocity_north,
169
+ "velocity_up": data.velocity_up,
170
+ "speed_accuracy": data.speed_accuracy,
171
+ },
172
+ }
173
+ )
174
+
175
+
176
+ class GoProGPSSampleEntry(CAMMSampleEntry):
177
+ camm_type: CAMMType = CAMMType.MLY_GOPRO_GPS
178
+
179
+ construct = C.Struct(
180
+ "latitude" / Double, # type: ignore
181
+ "longitude" / Double, # type: ignore
182
+ "altitude" / Float, # type: ignore
183
+ "epoch_time" / Double, # type: ignore
184
+ "fix" / C.Int32sl, # type: ignore
185
+ "precision" / Float, # type: ignore
186
+ "ground_speed" / Float, # type: ignore
187
+ )
188
+
189
+ telemetry_cls = telemetry.GPSPoint
190
+
191
+ @classmethod
192
+ def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GPSPoint:
193
+ return telemetry.GPSPoint(
194
+ time=sample.exact_time,
195
+ lat=data.latitude,
196
+ lon=data.longitude,
197
+ alt=data.altitude,
198
+ angle=None,
199
+ epoch_time=data.epoch_time,
200
+ fix=telemetry.GPSFix(data.fix),
201
+ precision=data.precision,
202
+ ground_speed=data.ground_speed,
203
+ )
204
+
205
+ @classmethod
206
+ def serialize(cls, data: telemetry.GPSPoint) -> bytes:
207
+ cls.serializable(data, throw=True)
208
+
209
+ if data.fix is None:
210
+ gps_fix = telemetry.GPSFix.NO_FIX.value
211
+ else:
212
+ gps_fix = data.fix.value
213
+
214
+ return CAMMSampleData.build(
215
+ {
216
+ "type": cls.camm_type.value,
217
+ "data": {
218
+ "latitude": data.lat,
219
+ "longitude": data.lon,
220
+ "altitude": -1.0 if data.alt is None else data.alt,
221
+ "epoch_time": data.epoch_time,
222
+ "fix": gps_fix,
223
+ "precision": data.precision,
224
+ "ground_speed": data.ground_speed,
225
+ },
226
+ }
110
227
  )
111
- elif box.type == CAMMType.ACCELERATION.value:
228
+
229
+
230
+ class AccelerationSampleEntry(CAMMSampleEntry):
231
+ camm_type: CAMMType = CAMMType.ACCELERATION
232
+
233
+ construct: C.Struct = Float[3] # type: ignore
234
+
235
+ telemetry_cls = telemetry.AccelerationData
236
+
237
+ @classmethod
238
+ def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.AccelerationData:
112
239
  return telemetry.AccelerationData(
113
240
  time=sample.exact_time,
114
- x=box.data[0],
115
- y=box.data[1],
116
- z=box.data[2],
241
+ x=data[0],
242
+ y=data[1],
243
+ z=data[2],
117
244
  )
118
- elif box.type == CAMMType.GYRO.value:
245
+
246
+ @classmethod
247
+ def serialize(cls, data: telemetry.AccelerationData) -> bytes:
248
+ cls.serializable(data, throw=True)
249
+
250
+ return CAMMSampleData.build(
251
+ {
252
+ "type": cls.camm_type.value,
253
+ "data": [data.x, data.y, data.z],
254
+ }
255
+ )
256
+
257
+
258
+ class GyroscopeSampleEntry(CAMMSampleEntry):
259
+ camm_type: CAMMType = CAMMType.GYRO
260
+
261
+ construct: C.Struct = Float[3] # type: ignore
262
+
263
+ telemetry_cls = telemetry.GyroscopeData
264
+
265
+ @classmethod
266
+ def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GyroscopeData:
119
267
  return telemetry.GyroscopeData(
120
268
  time=sample.exact_time,
121
- x=box.data[0],
122
- y=box.data[1],
123
- z=box.data[2],
269
+ x=data[0],
270
+ y=data[1],
271
+ z=data[2],
124
272
  )
125
- elif box.type == CAMMType.MAGNETIC_FIELD.value:
273
+
274
+ @classmethod
275
+ def serialize(cls, data: telemetry.GyroscopeData) -> bytes:
276
+ cls.serializable(data)
277
+
278
+ return CAMMSampleData.build(
279
+ {
280
+ "type": cls.camm_type.value,
281
+ "data": [data.x, data.y, data.z],
282
+ }
283
+ )
284
+
285
+
286
+ class MagnetometerSampleEntry(CAMMSampleEntry):
287
+ camm_type: CAMMType = CAMMType.MAGNETIC_FIELD
288
+
289
+ construct: C.Struct = Float[3] # type: ignore
290
+
291
+ telemetry_cls = telemetry.MagnetometerData
292
+
293
+ @classmethod
294
+ def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.MagnetometerData:
126
295
  return telemetry.MagnetometerData(
127
296
  time=sample.exact_time,
128
- x=box.data[0],
129
- y=box.data[1],
130
- z=box.data[2],
297
+ x=data[0],
298
+ y=data[1],
299
+ z=data[2],
131
300
  )
132
- return None
301
+
302
+ @classmethod
303
+ def serialize(cls, data: telemetry.MagnetometerData) -> bytes:
304
+ cls.serializable(data)
305
+
306
+ return CAMMSampleData.build(
307
+ {
308
+ "type": cls.camm_type.value,
309
+ "data": [data.x, data.y, data.z],
310
+ }
311
+ )
312
+
313
+
314
+ SAMPLE_ENTRY_CLS_BY_CAMM_TYPE = {
315
+ sample_entry_cls.camm_type: sample_entry_cls
316
+ for sample_entry_cls in CAMMSampleEntry.__subclasses__()
317
+ }
318
+ assert len(SAMPLE_ENTRY_CLS_BY_CAMM_TYPE) == 6, SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.keys()
319
+
320
+
321
+ _SWITCH: T.Dict[int, C.Struct] = {
322
+ # angle_axis
323
+ CAMMType.ANGLE_AXIS.value: Float[3], # type: ignore
324
+ CAMMType.EXPOSURE_TIME.value: C.Struct(
325
+ "pixel_exposure_time" / C.Int32sl, # type: ignore
326
+ "rolling_shutter_skew_time" / C.Int32sl, # type: ignore
327
+ ),
328
+ # position
329
+ CAMMType.POSITION.value: Float[3], # type: ignore
330
+ **{t.value: cls.construct for t, cls in SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.items()},
331
+ }
332
+
333
+ CAMMSampleData = C.Struct(
334
+ C.Padding(2),
335
+ "type" / C.Int16ul,
336
+ "data" / C.Switch(C.this.type, _SWITCH),
337
+ )
338
+
339
+
340
+ def _parse_telemetry_from_sample(
341
+ fp: T.BinaryIO, sample: Sample
342
+ ) -> T.Optional[TelemetryMeasurement]:
343
+ fp.seek(sample.raw_sample.offset, io.SEEK_SET)
344
+ data = fp.read(sample.raw_sample.size)
345
+ box = CAMMSampleData.parse(data)
346
+
347
+ camm_type = CAMMType(box.type) # type: ignore
348
+ SampleKlass = SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.get(camm_type)
349
+ if SampleKlass is None:
350
+ return None
351
+ return SampleKlass.deserialize(sample, box.data)
133
352
 
134
353
 
135
354
  def _filter_telemetry_by_elst_segments(
@@ -25,7 +25,13 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGeneric):
25
25
  num_processes: T.Optional[int] = None,
26
26
  ):
27
27
  super().__init__()
28
- tracks = parse_gpx(source_path)
28
+ try:
29
+ tracks = parse_gpx(source_path)
30
+ except Exception as ex:
31
+ raise RuntimeError(
32
+ f"Error parsing GPX {source_path}: {ex.__class__.__name__}: {ex}"
33
+ )
34
+
29
35
  if 1 < len(tracks):
30
36
  LOG.warning(
31
37
  "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
@@ -252,9 +252,24 @@ def _gps9_timestamp_to_epoch_time(
252
252
  return epoch_time
253
253
 
254
254
 
255
+ def _get_gps_type(input) -> bytes:
256
+ final = b""
257
+ for val in input or []:
258
+ if isinstance(val, bytes):
259
+ final += val
260
+ elif isinstance(val, list):
261
+ final += _get_gps_type(val)
262
+ else:
263
+ raise ValueError(f"Unexpected type {type(val)} in {input}")
264
+
265
+ return final
266
+
267
+
255
268
  def gps9_from_stream(
256
269
  stream: T.Sequence[KLVDict],
257
270
  ) -> T.Generator[GPSPoint, None, None]:
271
+ NUM_VALUES = 9
272
+
258
273
  indexed: T.Dict[bytes, T.List[T.List[T.Any]]] = {
259
274
  klv["key"]: klv["data"] for klv in stream
260
275
  }
@@ -270,17 +285,25 @@ def gps9_from_stream(
270
285
  if any(s == 0 for s in scal_values):
271
286
  return
272
287
 
273
- type = indexed.get(b"TYPE")
274
- if type is None:
288
+ gps_value_types = _get_gps_type(indexed.get(b"TYPE"))
289
+ if not gps_value_types:
275
290
  return
276
- gps_value_types = type[0]
291
+
292
+ if len(gps_value_types) != NUM_VALUES:
293
+ raise ValueError(
294
+ f"Error parsing the complex type {gps_value_types!r}: expect {NUM_VALUES} types but got {len(gps_value_types)}"
295
+ )
277
296
 
278
297
  try:
279
298
  sample_parser = C.Sequence(
280
- *[_type_mapping[t.to_bytes()][0] for t in gps_value_types]
299
+ *[
300
+ # Changed in version 3.11: Added default argument values for length and byteorder
301
+ _type_mapping[t.to_bytes(length=1, byteorder="big")][0]
302
+ for t in gps_value_types
303
+ ]
281
304
  )
282
305
  except Exception as ex:
283
- raise ValueError(f"Error parsing the complex type {gps_value_types}: {ex}")
306
+ raise ValueError(f"Error parsing the complex type {gps_value_types!r}: {ex}")
284
307
 
285
308
  for sample_data_bytes in gps9:
286
309
  sample_data = sample_parser.parse(sample_data_bytes)
@@ -12,14 +12,6 @@ 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
16
  class TelemetryMeasurement:
25
17
  """Base class for all telemetry measurements.
@@ -32,6 +24,26 @@ class TelemetryMeasurement:
32
24
  time: float
33
25
 
34
26
 
27
+ @dataclasses.dataclass
28
+ class GPSPoint(TelemetryMeasurement, 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(TelemetryMeasurement, 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
48
  class GyroscopeData(TelemetryMeasurement):
37
49
  """Gyroscope signal in radians/seconds around XYZ axes of the camera."""
mapillary_tools/upload.py CHANGED
@@ -18,14 +18,13 @@ from . import (
18
18
  exceptions,
19
19
  history,
20
20
  ipc,
21
- telemetry,
22
21
  types,
23
22
  upload_api_v4,
24
23
  uploader,
25
24
  utils,
26
25
  VERSION,
27
26
  )
28
- from .camm import camm_builder
27
+ from .camm import camm_builder, camm_parser
29
28
  from .geotag import gpmf_parser
30
29
  from .mp4 import simple_mp4_builder
31
30
  from .types import FileType
@@ -616,7 +615,7 @@ def upload(
616
615
  assert isinstance(video_metadata.md5sum, str), "md5sum should be updated"
617
616
 
618
617
  # extract telemetry measurements from GoPro videos
619
- telemetry_measurements: T.List[telemetry.TelemetryMeasurement] = []
618
+ telemetry_measurements: T.List[camm_parser.TelemetryMeasurement] = []
620
619
  if MAPILLARY__EXPERIMENTAL_ENABLE_IMU == "YES":
621
620
  if video_metadata.filetype is FileType.GOPRO:
622
621
  with video_metadata.filename.open("rb") as fp:
@@ -13,8 +13,12 @@ class CammParser(BaseParser):
13
13
  parser_label = "camm"
14
14
 
15
15
  @functools.cached_property
16
- def __camera_info(self) -> T.Tuple[str, str]:
17
- with self.videoPath.open("rb") as fp:
16
+ def _camera_info(self) -> T.Tuple[str, str]:
17
+ source_path = self.geotag_source_path
18
+ if not source_path:
19
+ return "", ""
20
+
21
+ with source_path.open("rb") as fp:
18
22
  return camm_parser.extract_camera_make_and_model(fp)
19
23
 
20
24
  def extract_points(self) -> T.Sequence[geo.Point]:
@@ -28,15 +32,7 @@ class CammParser(BaseParser):
28
32
  return []
29
33
 
30
34
  def extract_make(self) -> T.Optional[str]:
31
- source_path = self.geotag_source_path
32
- if not source_path:
33
- return None
34
- with source_path.open("rb") as _fp:
35
- return self.__camera_info[0] or None
35
+ return self._camera_info[0] or None
36
36
 
37
37
  def extract_model(self) -> T.Optional[str]:
38
- source_path = self.geotag_source_path
39
- if not source_path:
40
- return None
41
- with source_path.open("rb") as _fp:
42
- return self.__camera_info[1] or None
38
+ return self._camera_info[1] or None
@@ -20,7 +20,13 @@ class GpxParser(BaseParser):
20
20
  if not path:
21
21
  return []
22
22
 
23
- gpx_tracks = geotag_images_from_gpx_file.parse_gpx(path)
23
+ try:
24
+ gpx_tracks = geotag_images_from_gpx_file.parse_gpx(path)
25
+ except Exception as ex:
26
+ raise RuntimeError(
27
+ f"Error parsing GPX {path}: {ex.__class__.__name__}: {ex}"
28
+ )
29
+
24
30
  if 1 < len(gpx_tracks):
25
31
  LOG.warning(
26
32
  "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
@@ -32,40 +38,71 @@ class GpxParser(BaseParser):
32
38
  if not gpx_points:
33
39
  return gpx_points
34
40
 
41
+ offset = self._synx_gpx_by_first_gps_timestamp(gpx_points)
42
+
43
+ self._rebase_times(gpx_points, offset=offset)
44
+
45
+ return gpx_points
46
+
47
+ def _synx_gpx_by_first_gps_timestamp(
48
+ self, gpx_points: T.Sequence[geo.Point]
49
+ ) -> float:
50
+ offset: float = 0.0
51
+
52
+ if not gpx_points:
53
+ return offset
54
+
35
55
  first_gpx_dt = datetime.datetime.fromtimestamp(
36
56
  gpx_points[0].time, tz=datetime.timezone.utc
37
57
  )
38
58
  LOG.info("First GPX timestamp: %s", first_gpx_dt)
39
59
 
40
60
  # Extract first GPS timestamp (if found) for synchronization
41
- offset: float = 0.0
42
- parser = GenericVideoParser(self.videoPath, self.options, self.parserOptions)
61
+ # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
62
+ parser = GenericVideoParser(self.videoPath, self.options, {})
43
63
  gps_points = parser.extract_points()
44
- if gps_points:
45
- first_gps_point = gps_points[0]
46
- if isinstance(first_gps_point, telemetry.GPSPoint):
47
- if first_gps_point.epoch_time is not None:
48
- first_gps_dt = datetime.datetime.fromtimestamp(
49
- first_gps_point.epoch_time, tz=datetime.timezone.utc
50
- )
51
- LOG.info("First GPS timestamp: %s", first_gps_dt)
52
- offset = gpx_points[0].time - first_gps_point.epoch_time
53
- if offset:
54
- LOG.warning(
55
- "Found offset between GPX %s and video GPS timestamps %s: %s seconds",
56
- first_gpx_dt,
57
- first_gps_dt,
58
- offset,
59
- )
60
64
 
61
- self._rebase_times(gpx_points, offset=offset)
65
+ if not gps_points:
66
+ LOG.warning(
67
+ "Skip GPX synchronization because no GPS found in video %s",
68
+ self.videoPath,
69
+ )
70
+ return offset
62
71
 
63
- return gpx_points
72
+ first_gps_point = gps_points[0]
73
+ if isinstance(first_gps_point, telemetry.GPSPoint):
74
+ if first_gps_point.epoch_time is not None:
75
+ first_gps_dt = datetime.datetime.fromtimestamp(
76
+ first_gps_point.epoch_time, tz=datetime.timezone.utc
77
+ )
78
+ LOG.info("First GPS timestamp: %s", first_gps_dt)
79
+ offset = gpx_points[0].time - first_gps_point.epoch_time
80
+ if offset:
81
+ LOG.warning(
82
+ "Found offset between GPX %s and video GPS timestamps %s: %s seconds",
83
+ first_gpx_dt,
84
+ first_gps_dt,
85
+ offset,
86
+ )
87
+ else:
88
+ LOG.info(
89
+ "GPX and GPS are perfectly synchronized (all starts from %s)",
90
+ first_gpx_dt,
91
+ )
92
+ else:
93
+ LOG.warning(
94
+ "Skip GPX synchronization because no GPS epoch time found in video %s",
95
+ self.videoPath,
96
+ )
97
+
98
+ return offset
64
99
 
65
100
  def extract_make(self) -> T.Optional[str]:
66
- parser = GenericVideoParser(self.videoPath, self.options, self.parserOptions)
101
+ # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
102
+ parser = GenericVideoParser(self.videoPath, self.options, {})
67
103
  return parser.extract_make()
68
104
 
69
105
  def extract_model(self) -> T.Optional[str]:
70
- parser = GenericVideoParser(self.videoPath, self.options, self.parserOptions)
106
+ # Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
107
+ parser = GenericVideoParser(self.videoPath, self.options, {})
71
108
  return parser.extract_model()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mapillary_tools
3
- Version: 0.13.0
3
+ Version: 0.13.1
4
4
  Summary: Mapillary Image/Video Import Pipeline
5
5
  Home-page: https://github.com/mapillary/mapillary_tools
6
6
  Author: Mapillary
@@ -1,4 +1,4 @@
1
- mapillary_tools/__init__.py,sha256=hZfMXyZ8nUrpqLa-dZCy9tXhu5fH1SZJKEGUd0caUDw,19
1
+ mapillary_tools/__init__.py,sha256=XBFY-aPf1nqJ7U8CvnYiXTRu3uDJLRQjOxibBTi-54U,19
2
2
  mapillary_tools/api_v4.py,sha256=zhRtgx3EnzgqtjziRhvFq3ONvsPaB9hROsuKFcf_pFo,5197
3
3
  mapillary_tools/authenticate.py,sha256=LCFcs6LqZmXaYkTUEKgGfmqytWdh5v_L3KXB48ojOZ4,3090
4
4
  mapillary_tools/config.py,sha256=jCjaK4jJaTY4AV4qf_b_tcxn5LA_uPsEWlGIdm2zw6g,2103
@@ -15,14 +15,14 @@ mapillary_tools/ipc.py,sha256=DwWQb9hNshx0bg0Fo5NjY0mXjs-FkbR6tIQmjMgMtmg,1089
15
15
  mapillary_tools/process_geotag_properties.py,sha256=w4hhv_c4sRydCK9QCO50sT2yo2zeVlY7dSdXQ93InFc,23159
16
16
  mapillary_tools/process_sequence_properties.py,sha256=5oYEjz9crnLVQtCkxbwn57TkeuHFbBh_zQXQSA4ENWg,11561
17
17
  mapillary_tools/sample_video.py,sha256=dpdX7bUNEmcrz-3gh3Y3awnTDX66pChbTKuF8qGfeCI,14400
18
- mapillary_tools/telemetry.py,sha256=WpBGPF_GMPjM_EFqXIutFtpDFL9wj7yEzGNGnfQZUo8,1255
18
+ mapillary_tools/telemetry.py,sha256=3a7MvTH4Rr_mXp2NInubPIS8JFOuBeQC7PC2prfXNfI,1559
19
19
  mapillary_tools/types.py,sha256=6kww2UdKM6YzabYbc862BYzEWtxL2hhxCRFfeDiUtF0,22074
20
- mapillary_tools/upload.py,sha256=8dQ3ZWsjau1_xZN3ssjGGkBnLKbKIhjC91-zWstYlD8,24439
20
+ mapillary_tools/upload.py,sha256=ab1WEut6qv352njWKtNbUXmV8KShlApOjcwb1FpcC_Q,24439
21
21
  mapillary_tools/upload_api_v4.py,sha256=1WvoUis92KDXbqfoyvyyDmiCqwXezYkMJZhnYaVm3BA,8560
22
22
  mapillary_tools/uploader.py,sha256=VieDKi51wdXTIhN7x_mcuQeHESUyFlF5cgB-TAnF4g0,14093
23
23
  mapillary_tools/utils.py,sha256=VNtK1tAb3Hh8y3P5e5Y3iewREkIoLDa3C2myRYcF2lY,5970
24
- mapillary_tools/camm/camm_builder.py,sha256=TXZfhu3xGjtrLEWnB14D7aSOrHOoSJef24YSLApiIfY,10631
25
- mapillary_tools/camm/camm_parser.py,sha256=RaCWeLvS_AyHD6B6wDUu9DAsdfByVHMAPTqEqjtFibE,9734
24
+ mapillary_tools/camm/camm_builder.py,sha256=x4WF-n6zOEUhK1Poo7du004-7DQhjnf598ZNfM3r7iA,9075
25
+ mapillary_tools/camm/camm_parser.py,sha256=ARwImVcXY1FckCnPLPoyYAuODJb5iZI0hlQurAIeiWQ,16473
26
26
  mapillary_tools/commands/__init__.py,sha256=41CFrPLGlG3566uhxssEF3TGAtSpADFPPcDMHbViU0E,171
27
27
  mapillary_tools/commands/__main__.py,sha256=VdWkx1ekPH-88Ybe78IcO9FWpZ5cUhsbGRw7LuzQObU,4832
28
28
  mapillary_tools/commands/authenticate.py,sha256=4aVvAQal_mqtm2NEMBt5aKLahi0iRdO8b7WSBf6jokA,1136
@@ -40,13 +40,13 @@ mapillary_tools/geotag/geotag_images_from_exif.py,sha256=hCgBwZABk2tbBQC3cHQBV5p
40
40
  mapillary_tools/geotag/geotag_images_from_exiftool.py,sha256=a-c4H8VIyPdJkfUIvJho0phR0QU0zN8-lSyiCz0wc4s,3981
41
41
  mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py,sha256=nRVAjgTJwx_eCaSBpPCgcIaZs3EYgGueYxSS9XhKv40,3350
42
42
  mapillary_tools/geotag/geotag_images_from_gpx.py,sha256=S9Pw6FvP5kRSpHUnKUYKXmw0CHa9V92UmrS_MJfbjS4,9053
43
- mapillary_tools/geotag/geotag_images_from_gpx_file.py,sha256=zEbC0kVf_iw9ioIyJLL-gYN_QvAOEdAoEczCBkizl38,5122
43
+ mapillary_tools/geotag/geotag_images_from_gpx_file.py,sha256=-vTbZ1HufZzJCd8VvukdTjsJRcymtfld2W5t65VSG5E,5300
44
44
  mapillary_tools/geotag/geotag_images_from_nmea_file.py,sha256=dDdHnJInQ_WN3ZRf-w44NSBElDLPs7XYBiimvE2iCNo,1651
45
45
  mapillary_tools/geotag/geotag_images_from_video.py,sha256=XsaWOFChGItl-j1UbKM4hNjUqN29pVNbMpGT_BvI-o8,3306
46
46
  mapillary_tools/geotag/geotag_videos_from_exiftool_video.py,sha256=fkkWou1WFt3ft024399vis9No2cxrwot7Pg5HBw7o7s,5225
47
47
  mapillary_tools/geotag/geotag_videos_from_video.py,sha256=mqBZKUEkqT96nOzl5LJxzzTKuKsnAkMK5lH8k3oY3YE,7330
48
48
  mapillary_tools/geotag/gpmf_gps_filter.py,sha256=7cg8wEjC1DrujKY76FZguXsaPqTRkG9-t32OeuOJQIc,2755
49
- mapillary_tools/geotag/gpmf_parser.py,sha256=yAXzFAlj4VsbFju-kNDBxhT1K6kfmZL37lrM26Xkg0M,22514
49
+ mapillary_tools/geotag/gpmf_parser.py,sha256=6Q38oeSd2kHTs44Fzpg9R555EbQqdd5QcIuIZdG4z_o,23233
50
50
  mapillary_tools/geotag/gps_filter.py,sha256=4CPL8glxdzPWIbfGPPgyqMLyiZFt-djce5vhiFPuZB8,3766
51
51
  mapillary_tools/geotag/utils.py,sha256=Orl35Df4ypxj4v6Lu1Mhk9d2XZxa8yNffz1s1C0JZsQ,651
52
52
  mapillary_tools/mp4/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -60,16 +60,16 @@ mapillary_tools/video_data_extraction/extract_video_data.py,sha256=_2BBdSYeYKR4B
60
60
  mapillary_tools/video_data_extraction/video_data_parser_factory.py,sha256=qaJHvLgwI5lukJncMd8ggxeSxXOiVzBSJO5GlGQYiXY,1134
61
61
  mapillary_tools/video_data_extraction/extractors/base_parser.py,sha256=s7Xuwg4I5JZ27oL4ebMSdo093plAXfZ-6uDQ_h97WHY,2134
62
62
  mapillary_tools/video_data_extraction/extractors/blackvue_parser.py,sha256=jAcGyF6PML2EdJ4zle8cR12QeTRZc5qxlz8_4gcTZPU,1089
63
- mapillary_tools/video_data_extraction/extractors/camm_parser.py,sha256=sr8oDbC9lGyHHp8qkpQBmWOf4U_umlokaJM0kPVKgzw,1314
63
+ mapillary_tools/video_data_extraction/extractors/camm_parser.py,sha256=YMiViocXSVlfn8_qm1jcwSJhnnEaK8v5ADHwo2YXe10,1117
64
64
  mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py,sha256=PFNCRk9pGrPIfVwLMcnzmVNMITVjNHhbrOOMwxaSstg,2270
65
65
  mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py,sha256=Tt0h4TiCKocERWMlRXzlpoaA_WJ_4b20MgMLGYNl4AM,1734
66
66
  mapillary_tools/video_data_extraction/extractors/generic_video_parser.py,sha256=34O6Km5kNDoJNJtIUOwtAzzMntuqkSZJfeli7caWSkA,1693
67
67
  mapillary_tools/video_data_extraction/extractors/gopro_parser.py,sha256=IVnTyquSraTUaG9rxbJfVWc1-drdY5PaHn5urh3IBk4,1325
68
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py,sha256=FRysFhN2aa0MGnzLO7pcZkwo8790GUelIZg9pddZvic,2594
68
+ mapillary_tools/video_data_extraction/extractors/gpx_parser.py,sha256=FNrdnXl48k8I1I5fGwYsClhfFEHVsooRLRboUYECv3I,3811
69
69
  mapillary_tools/video_data_extraction/extractors/nmea_parser.py,sha256=raSXavBvP-0LJCB_TwLL0mOv2uHSsB744igTsaKAaGc,658
70
- mapillary_tools-0.13.0.dist-info/LICENSE,sha256=l2D8cKfFmmJq_wcVq_JElPJrlvWQOzNWx7gMLINucxc,1292
71
- mapillary_tools-0.13.0.dist-info/METADATA,sha256=IjaJljFtCVqGSVhabQ2MptJlan76aUoGLulB1ecs9-E,19758
72
- mapillary_tools-0.13.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
73
- mapillary_tools-0.13.0.dist-info/entry_points.txt,sha256=A3f3LP-BO_P-U8Y29QfpT4jx6Mjk3sXjTi2Yew4bvj8,75
74
- mapillary_tools-0.13.0.dist-info/top_level.txt,sha256=FbDkMgOrt1S70ho1WSBrOwzKOSkJFDwwqFOoY5-527s,16
75
- mapillary_tools-0.13.0.dist-info/RECORD,,
70
+ mapillary_tools-0.13.1.dist-info/LICENSE,sha256=l2D8cKfFmmJq_wcVq_JElPJrlvWQOzNWx7gMLINucxc,1292
71
+ mapillary_tools-0.13.1.dist-info/METADATA,sha256=K1pmlXXrkP_o1qFddzeG6RyXGt6HqVKhU8aLhIVUK_w,19758
72
+ mapillary_tools-0.13.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
73
+ mapillary_tools-0.13.1.dist-info/entry_points.txt,sha256=A3f3LP-BO_P-U8Y29QfpT4jx6Mjk3sXjTi2Yew4bvj8,75
74
+ mapillary_tools-0.13.1.dist-info/top_level.txt,sha256=FbDkMgOrt1S70ho1WSBrOwzKOSkJFDwwqFOoY5-527s,16
75
+ mapillary_tools-0.13.1.dist-info/RECORD,,