mapillary-tools 0.13.0__tar.gz → 0.13.1__tar.gz

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 (86) hide show
  1. {mapillary_tools-0.13.0/mapillary_tools.egg-info → mapillary_tools-0.13.1}/PKG-INFO +1 -1
  2. mapillary_tools-0.13.1/mapillary_tools/__init__.py +1 -0
  3. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/camm/camm_builder.py +14 -62
  4. mapillary_tools-0.13.1/mapillary_tools/camm/camm_parser.py +561 -0
  5. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
  6. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gpmf_parser.py +28 -5
  7. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/telemetry.py +20 -8
  8. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/upload.py +2 -3
  9. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/camm_parser.py +8 -12
  10. mapillary_tools-0.13.1/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +108 -0
  11. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1/mapillary_tools.egg-info}/PKG-INFO +1 -1
  12. mapillary_tools-0.13.0/mapillary_tools/__init__.py +0 -1
  13. mapillary_tools-0.13.0/mapillary_tools/camm/camm_parser.py +0 -342
  14. mapillary_tools-0.13.0/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -71
  15. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/LICENSE +0 -0
  16. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/MANIFEST.in +0 -0
  17. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/README.md +0 -0
  18. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/api_v4.py +0 -0
  19. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/authenticate.py +0 -0
  20. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/__init__.py +0 -0
  21. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/__main__.py +0 -0
  22. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/authenticate.py +0 -0
  23. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/process.py +0 -0
  24. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/process_and_upload.py +0 -0
  25. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/sample_video.py +0 -0
  26. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/upload.py +0 -0
  27. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/video_process.py +0 -0
  28. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/video_process_and_upload.py +0 -0
  29. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/zip.py +0 -0
  30. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/config.py +0 -0
  31. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/constants.py +0 -0
  32. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exceptions.py +0 -0
  33. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exif_read.py +0 -0
  34. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exif_write.py +0 -0
  35. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exiftool_read.py +0 -0
  36. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exiftool_read_video.py +0 -0
  37. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/ffmpeg.py +0 -0
  38. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geo.py +0 -0
  39. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/__init__.py +0 -0
  40. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/blackvue_parser.py +0 -0
  41. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_from_generic.py +0 -0
  42. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
  43. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
  44. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -0
  45. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
  46. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
  47. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
  48. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -0
  49. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
  50. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gpmf_gps_filter.py +0 -0
  51. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gps_filter.py +0 -0
  52. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/utils.py +0 -0
  53. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/history.py +0 -0
  54. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/ipc.py +0 -0
  55. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/__init__.py +0 -0
  56. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/construct_mp4_parser.py +0 -0
  57. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/io_utils.py +0 -0
  58. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
  59. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
  60. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
  61. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/process_geotag_properties.py +0 -0
  62. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/process_sequence_properties.py +0 -0
  63. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/sample_video.py +0 -0
  64. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/types.py +0 -0
  65. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/upload_api_v4.py +0 -0
  66. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/uploader.py +0 -0
  67. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/utils.py +0 -0
  68. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/cli_options.py +0 -0
  69. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extract_video_data.py +0 -0
  70. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -0
  71. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -0
  72. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -0
  73. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -0
  74. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -0
  75. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -0
  76. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -0
  77. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -0
  78. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/SOURCES.txt +0 -0
  79. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/dependency_links.txt +0 -0
  80. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/entry_points.txt +0 -0
  81. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/requires.txt +0 -0
  82. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/top_level.txt +0 -0
  83. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/requirements.txt +0 -0
  84. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/schema/image_description_schema.json +0 -0
  85. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/setup.cfg +0 -0
  86. {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/setup.py +0 -0
@@ -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
@@ -0,0 +1 @@
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,
@@ -0,0 +1,561 @@
1
+ # pyre-ignore-all-errors[5, 11, 16, 21, 24, 58]
2
+
3
+ import abc
4
+ import dataclasses
5
+ import io
6
+ import logging
7
+ import pathlib
8
+ import typing as T
9
+ from enum import Enum
10
+
11
+ import construct as C
12
+
13
+ from .. import geo, telemetry
14
+ from ..mp4 import simple_mp4_parser as sparser
15
+ from ..mp4.mp4_sample_parser import MovieBoxParser, Sample, TrackBoxParser
16
+
17
+
18
+ LOG = logging.getLogger(__name__)
19
+
20
+
21
+ TelemetryMeasurement = T.Union[
22
+ geo.Point,
23
+ telemetry.TelemetryMeasurement,
24
+ ]
25
+
26
+
27
+ # Camera Motion Metadata Spec https://developers.google.com/streetview/publish/camm-spec
28
+ class CAMMType(Enum):
29
+ ANGLE_AXIS = 0
30
+ EXPOSURE_TIME = 1
31
+ GYRO = 2
32
+ ACCELERATION = 3
33
+ POSITION = 4
34
+ MIN_GPS = 5
35
+ GPS = 6
36
+ MAGNETIC_FIELD = 7
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
+
43
+
44
+ # All fields are little-endian
45
+ Float = C.Float32l
46
+ Double = C.Float64l
47
+
48
+
49
+ TTelemetry = T.TypeVar("TTelemetry", bound=TelemetryMeasurement)
50
+
51
+
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:
91
+ return geo.Point(
92
+ time=sample.exact_time,
93
+ lat=data[0],
94
+ lon=data[1],
95
+ alt=data[2],
96
+ angle=None,
97
+ )
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(
137
+ time=sample.exact_time,
138
+ lat=data.latitude,
139
+ lon=data.longitude,
140
+ alt=data.altitude,
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
+ }
227
+ )
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:
239
+ return telemetry.AccelerationData(
240
+ time=sample.exact_time,
241
+ x=data[0],
242
+ y=data[1],
243
+ z=data[2],
244
+ )
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:
267
+ return telemetry.GyroscopeData(
268
+ time=sample.exact_time,
269
+ x=data[0],
270
+ y=data[1],
271
+ z=data[2],
272
+ )
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:
295
+ return telemetry.MagnetometerData(
296
+ time=sample.exact_time,
297
+ x=data[0],
298
+ y=data[1],
299
+ z=data[2],
300
+ )
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)
352
+
353
+
354
+ def _filter_telemetry_by_elst_segments(
355
+ measurements: T.Iterable[TelemetryMeasurement],
356
+ elst: T.Sequence[T.Tuple[float, float]],
357
+ ) -> T.Generator[TelemetryMeasurement, None, None]:
358
+ empty_elst = [entry for entry in elst if entry[0] == -1]
359
+ if empty_elst:
360
+ offset = empty_elst[-1][1]
361
+ else:
362
+ offset = 0
363
+
364
+ elst = [entry for entry in elst if entry[0] != -1]
365
+
366
+ if not elst:
367
+ for m in measurements:
368
+ yield dataclasses.replace(m, time=m.time + offset)
369
+ return
370
+
371
+ elst.sort(key=lambda entry: entry[0])
372
+ elst_idx = 0
373
+ for m in measurements:
374
+ if len(elst) <= elst_idx:
375
+ break
376
+ media_time, duration = elst[elst_idx]
377
+ if m.time < media_time:
378
+ pass
379
+ elif m.time <= media_time + duration:
380
+ yield dataclasses.replace(m, time=m.time + offset)
381
+ else:
382
+ elst_idx += 1
383
+
384
+
385
+ def elst_entry_to_seconds(
386
+ entry: T.Dict, movie_timescale: int, media_timescale: int
387
+ ) -> T.Tuple[float, float]:
388
+ assert movie_timescale > 0, "expected positive movie_timescale"
389
+ assert media_timescale > 0, "expected positive media_timescale"
390
+ media_time, duration = entry["media_time"], entry["segment_duration"]
391
+ if media_time != -1:
392
+ media_time = media_time / media_timescale
393
+ duration = duration / movie_timescale
394
+ return (media_time, duration)
395
+
396
+
397
+ def _is_camm_description(description: T.Dict) -> bool:
398
+ return description["format"] == b"camm"
399
+
400
+
401
+ def _contains_camm_description(track: TrackBoxParser) -> bool:
402
+ descriptions = track.extract_sample_descriptions()
403
+ return any(_is_camm_description(d) for d in descriptions)
404
+
405
+
406
+ def _filter_telemetry_by_track_elst(
407
+ moov: MovieBoxParser,
408
+ track: TrackBoxParser,
409
+ measurements: T.Iterable[TelemetryMeasurement],
410
+ ) -> T.List[TelemetryMeasurement]:
411
+ elst_boxdata = track.extract_elst_boxdata()
412
+
413
+ if elst_boxdata is not None:
414
+ elst_entries = elst_boxdata["entries"]
415
+ if elst_entries:
416
+ # media_timescale
417
+ mdhd_boxdata = track.extract_mdhd_boxdata()
418
+ media_timescale = mdhd_boxdata["timescale"]
419
+
420
+ # movie_timescale
421
+ mvhd_boxdata = moov.extract_mvhd_boxdata()
422
+ movie_timescale = mvhd_boxdata["timescale"]
423
+
424
+ segments = [
425
+ elst_entry_to_seconds(
426
+ entry,
427
+ movie_timescale=movie_timescale,
428
+ media_timescale=media_timescale,
429
+ )
430
+ for entry in elst_entries
431
+ ]
432
+
433
+ return list(_filter_telemetry_by_elst_segments(measurements, segments))
434
+
435
+ return list(measurements)
436
+
437
+
438
+ def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]:
439
+ """
440
+ Return a list of points (could be empty) if it is a valid CAMM video,
441
+ otherwise None
442
+ """
443
+
444
+ moov = MovieBoxParser.parse_stream(fp)
445
+
446
+ for track in moov.extract_tracks():
447
+ if _contains_camm_description(track):
448
+ maybe_measurements = (
449
+ _parse_telemetry_from_sample(fp, sample)
450
+ for sample in track.extract_samples()
451
+ if _is_camm_description(sample.description)
452
+ )
453
+ points = [m for m in maybe_measurements if isinstance(m, geo.Point)]
454
+
455
+ return T.cast(
456
+ T.List[geo.Point], _filter_telemetry_by_track_elst(moov, track, points)
457
+ )
458
+
459
+ return None
460
+
461
+
462
+ def extract_telemetry_data(fp: T.BinaryIO) -> T.Optional[T.List[TelemetryMeasurement]]:
463
+ moov = MovieBoxParser.parse_stream(fp)
464
+
465
+ for track in moov.extract_tracks():
466
+ if _contains_camm_description(track):
467
+ maybe_measurements = (
468
+ _parse_telemetry_from_sample(fp, sample)
469
+ for sample in track.extract_samples()
470
+ if _is_camm_description(sample.description)
471
+ )
472
+ measurements = [m for m in maybe_measurements if m is not None]
473
+
474
+ measurements = _filter_telemetry_by_track_elst(moov, track, measurements)
475
+
476
+ return measurements
477
+
478
+ return None
479
+
480
+
481
+ def parse_gpx(path: pathlib.Path) -> T.List[geo.Point]:
482
+ with path.open("rb") as fp:
483
+ points = extract_points(fp)
484
+ if points is None:
485
+ return []
486
+ return points
487
+
488
+
489
+ MakeOrModel = C.Struct(
490
+ "size" / C.Int16ub,
491
+ C.Padding(2),
492
+ "data" / C.FixedSized(C.this.size, C.GreedyBytes),
493
+ )
494
+
495
+
496
+ def _decode_quietly(data: bytes, h: sparser.Header) -> str:
497
+ try:
498
+ return data.decode("utf-8")
499
+ except UnicodeDecodeError:
500
+ LOG.warning("Failed to decode %s: %s", h, data[:512])
501
+ return ""
502
+
503
+
504
+ def _parse_quietly(data: bytes, h: sparser.Header) -> bytes:
505
+ try:
506
+ parsed = MakeOrModel.parse(data)
507
+ except C.ConstructError:
508
+ LOG.warning("Failed to parse %s: %s", h, data[:512])
509
+ return b""
510
+ return parsed["data"]
511
+
512
+
513
+ def extract_camera_make_and_model(fp: T.BinaryIO) -> T.Tuple[str, str]:
514
+ header_and_stream = sparser.parse_path(
515
+ fp,
516
+ [
517
+ b"moov",
518
+ b"udta",
519
+ [
520
+ # Insta360 Titan
521
+ b"\xa9mak",
522
+ b"\xa9mod",
523
+ # RICHO THETA V
524
+ b"@mod",
525
+ b"@mak",
526
+ # RICHO THETA V
527
+ b"manu",
528
+ b"modl",
529
+ ],
530
+ ],
531
+ )
532
+
533
+ make: T.Optional[str] = None
534
+ model: T.Optional[str] = None
535
+
536
+ try:
537
+ for h, s in header_and_stream:
538
+ data = s.read(h.maxsize)
539
+ if h.type == b"\xa9mak":
540
+ make_data = _parse_quietly(data, h)
541
+ make_data = make_data.rstrip(b"\x00")
542
+ make = _decode_quietly(make_data, h)
543
+ elif h.type == b"\xa9mod":
544
+ model_data = _parse_quietly(data, h)
545
+ model_data = model_data.rstrip(b"\x00")
546
+ model = _decode_quietly(model_data, h)
547
+ elif h.type in [b"@mak", b"manu"]:
548
+ make = _decode_quietly(data, h)
549
+ elif h.type in [b"@mod", b"modl"]:
550
+ model = _decode_quietly(data, h)
551
+ # quit when both found
552
+ if make and model:
553
+ break
554
+ except sparser.ParsingError:
555
+ pass
556
+
557
+ if make:
558
+ make = make.strip()
559
+ if model:
560
+ model = model.strip()
561
+ return make or "", model or ""
@@ -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",