mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +235 -14
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +11 -4
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# pyre-ignore-all-errors[5, 11, 16, 21, 24, 58]
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
4
|
+
import abc
|
|
3
5
|
import dataclasses
|
|
4
6
|
import io
|
|
5
7
|
import logging
|
|
6
|
-
import pathlib
|
|
7
8
|
import typing as T
|
|
8
9
|
from enum import Enum
|
|
9
10
|
|
|
10
11
|
import construct as C
|
|
12
|
+
from typing_extensions import TypeIs
|
|
11
13
|
|
|
12
14
|
from .. import geo, telemetry
|
|
13
|
-
from ..mp4 import simple_mp4_parser as sparser
|
|
14
15
|
from ..mp4.mp4_sample_parser import MovieBoxParser, Sample, TrackBoxParser
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
LOG = logging.getLogger(__name__)
|
|
19
|
+
# All fields are little-endian
|
|
20
|
+
_Float = C.Float32l
|
|
21
|
+
_Double = C.Float64l
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
TelemetryMeasurement = T.Union[
|
|
21
25
|
geo.Point,
|
|
22
|
-
telemetry.
|
|
23
|
-
telemetry.GyroscopeData,
|
|
24
|
-
telemetry.MagnetometerData,
|
|
26
|
+
telemetry.TimestampedMeasurement,
|
|
25
27
|
]
|
|
26
28
|
|
|
27
29
|
|
|
@@ -37,99 +39,399 @@ class CAMMType(Enum):
|
|
|
37
39
|
MAGNETIC_FIELD = 7
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
Float = C.Float32l
|
|
42
|
-
Double = C.Float64l
|
|
42
|
+
TTelemetry = T.TypeVar("TTelemetry", bound=TelemetryMeasurement)
|
|
43
43
|
|
|
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
44
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
45
|
+
@dataclasses.dataclass
|
|
46
|
+
class CAMMInfo:
|
|
47
|
+
# None indicates the data has been extracted,
|
|
48
|
+
# while [] indicates extracetd but no data point found
|
|
49
|
+
mini_gps: list[geo.Point] | None = None
|
|
50
|
+
gps: list[telemetry.CAMMGPSPoint] | None = None
|
|
51
|
+
accl: list[telemetry.AccelerationData] | None = None
|
|
52
|
+
gyro: list[telemetry.GyroscopeData] | None = None
|
|
53
|
+
magn: list[telemetry.MagnetometerData] | None = None
|
|
54
|
+
make: str = ""
|
|
55
|
+
model: str = ""
|
|
85
56
|
|
|
86
57
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
58
|
+
def extract_camm_info(fp: T.BinaryIO, telemetry_only: bool = False) -> CAMMInfo | None:
|
|
59
|
+
moov = MovieBoxParser.parse_stream(fp)
|
|
60
|
+
|
|
61
|
+
make, model = "", ""
|
|
62
|
+
if not telemetry_only:
|
|
63
|
+
udta_boxdata = moov.extract_udta_boxdata()
|
|
64
|
+
if udta_boxdata is not None:
|
|
65
|
+
make, model = _extract_camera_make_and_model_from_utda_boxdata(udta_boxdata)
|
|
66
|
+
|
|
67
|
+
gps_only_construct = _construct_with_selected_camm_types(
|
|
68
|
+
[CAMMType.MIN_GPS, CAMMType.GPS]
|
|
69
|
+
)
|
|
70
|
+
# Optimization: skip parsing sample data smaller than 16 bytes
|
|
71
|
+
# because we are only interested in MIN_GPS and GPS which are larger than 16 bytes
|
|
72
|
+
MIN_GPS_SAMPLE_SIZE = 17
|
|
73
|
+
|
|
74
|
+
for track in moov.extract_tracks():
|
|
75
|
+
if _contains_camm_description(track):
|
|
76
|
+
if telemetry_only:
|
|
77
|
+
maybe_measurements = (
|
|
78
|
+
_parse_telemetry_from_sample(fp, sample)
|
|
79
|
+
for sample in track.extract_samples()
|
|
80
|
+
if _is_camm_description(sample.description)
|
|
81
|
+
)
|
|
82
|
+
measurements = _filter_telemetry_by_track_elst(
|
|
83
|
+
moov, track, (m for m in maybe_measurements if m is not None)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
accl: list[telemetry.AccelerationData] = []
|
|
87
|
+
gyro: list[telemetry.GyroscopeData] = []
|
|
88
|
+
magn: list[telemetry.MagnetometerData] = []
|
|
89
|
+
|
|
90
|
+
for measurement in measurements:
|
|
91
|
+
if isinstance(measurement, telemetry.AccelerationData):
|
|
92
|
+
accl.append(measurement)
|
|
93
|
+
elif isinstance(measurement, telemetry.GyroscopeData):
|
|
94
|
+
gyro.append(measurement)
|
|
95
|
+
elif isinstance(measurement, telemetry.MagnetometerData):
|
|
96
|
+
magn.append(measurement)
|
|
97
|
+
|
|
98
|
+
return CAMMInfo(accl=accl, gyro=gyro, magn=magn)
|
|
99
|
+
else:
|
|
100
|
+
maybe_measurements = (
|
|
101
|
+
_parse_telemetry_from_sample(fp, sample, gps_only_construct)
|
|
102
|
+
for sample in track.extract_samples()
|
|
103
|
+
if _is_camm_description(sample.description)
|
|
104
|
+
and sample.raw_sample.size >= MIN_GPS_SAMPLE_SIZE
|
|
105
|
+
)
|
|
106
|
+
measurements = _filter_telemetry_by_track_elst(
|
|
107
|
+
moov, track, (m for m in maybe_measurements if m is not None)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
mini_gps: list[geo.Point] = []
|
|
111
|
+
gps: list[telemetry.CAMMGPSPoint] = []
|
|
112
|
+
|
|
113
|
+
for measurement in measurements:
|
|
114
|
+
if isinstance(measurement, geo.Point):
|
|
115
|
+
mini_gps.append(measurement)
|
|
116
|
+
elif isinstance(measurement, telemetry.CAMMGPSPoint):
|
|
117
|
+
gps.append(measurement)
|
|
118
|
+
|
|
119
|
+
return CAMMInfo(mini_gps=mini_gps, gps=gps, make=make, model=model)
|
|
120
|
+
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_camera_make_and_model(fp: T.BinaryIO) -> tuple[str, str]:
|
|
125
|
+
moov = MovieBoxParser.parse_stream(fp)
|
|
126
|
+
udta_boxdata = moov.extract_udta_boxdata()
|
|
127
|
+
if udta_boxdata is None:
|
|
128
|
+
return "", ""
|
|
129
|
+
return _extract_camera_make_and_model_from_utda_boxdata(udta_boxdata)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CAMMSampleEntry(abc.ABC, T.Generic[TTelemetry]):
|
|
133
|
+
serialized_camm_type: CAMMType
|
|
134
|
+
|
|
135
|
+
telemetry_cls_type: T.Type[TTelemetry]
|
|
136
|
+
|
|
137
|
+
construct: C.Struct
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def serializable(cls, data: T.Any, throw: bool = False) -> TypeIs[TTelemetry]:
|
|
141
|
+
# Use "is" for exact type match, instead of isinstance
|
|
142
|
+
if type(data) is cls.telemetry_cls_type:
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
if throw:
|
|
146
|
+
raise TypeError(
|
|
147
|
+
f"{cls} can not serialize {type(data)}: expect {cls.telemetry_cls_type}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
@abc.abstractmethod
|
|
154
|
+
def serialize(cls, data: TTelemetry) -> bytes:
|
|
155
|
+
raise NotImplementedError
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
@abc.abstractmethod
|
|
159
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> TTelemetry:
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class MinGPSSampleEntry(CAMMSampleEntry):
|
|
164
|
+
serialized_camm_type = CAMMType.MIN_GPS
|
|
165
|
+
|
|
166
|
+
telemetry_cls_type = geo.Point
|
|
167
|
+
|
|
168
|
+
construct = _Double[3] # type: ignore
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> geo.Point:
|
|
94
172
|
return geo.Point(
|
|
95
173
|
time=sample.exact_time,
|
|
96
|
-
lat=
|
|
97
|
-
lon=
|
|
98
|
-
alt=
|
|
174
|
+
lat=data[0],
|
|
175
|
+
lon=data[1],
|
|
176
|
+
alt=data[2],
|
|
99
177
|
angle=None,
|
|
100
178
|
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def serialize(cls, data: geo.Point) -> bytes:
|
|
182
|
+
cls.serializable(data, throw=True)
|
|
183
|
+
|
|
184
|
+
return CAMMSampleData.build(
|
|
185
|
+
{
|
|
186
|
+
"type": cls.serialized_camm_type.value,
|
|
187
|
+
"data": [
|
|
188
|
+
data.lat,
|
|
189
|
+
data.lon,
|
|
190
|
+
-1.0 if data.alt is None else data.alt,
|
|
191
|
+
],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class GPSSampleEntry(CAMMSampleEntry):
|
|
197
|
+
serialized_camm_type: CAMMType = CAMMType.GPS
|
|
198
|
+
|
|
199
|
+
telemetry_cls_type = telemetry.CAMMGPSPoint
|
|
200
|
+
|
|
201
|
+
construct = C.Struct(
|
|
202
|
+
"time_gps_epoch" / _Double, # type: ignore
|
|
203
|
+
"gps_fix_type" / C.Int32sl, # type: ignore
|
|
204
|
+
"latitude" / _Double, # type: ignore
|
|
205
|
+
"longitude" / _Double, # type: ignore
|
|
206
|
+
"altitude" / _Float, # type: ignore
|
|
207
|
+
"horizontal_accuracy" / _Float, # type: ignore
|
|
208
|
+
"vertical_accuracy" / _Float, # type: ignore
|
|
209
|
+
"velocity_east" / _Float, # type: ignore
|
|
210
|
+
"velocity_north" / _Float, # type: ignore
|
|
211
|
+
"velocity_up" / _Float, # type: ignore
|
|
212
|
+
"speed_accuracy" / _Float, # type: ignore
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.CAMMGPSPoint:
|
|
217
|
+
return telemetry.CAMMGPSPoint(
|
|
105
218
|
time=sample.exact_time,
|
|
106
|
-
lat=
|
|
107
|
-
lon=
|
|
108
|
-
alt=
|
|
219
|
+
lat=data.latitude,
|
|
220
|
+
lon=data.longitude,
|
|
221
|
+
alt=data.altitude,
|
|
109
222
|
angle=None,
|
|
223
|
+
time_gps_epoch=data.time_gps_epoch,
|
|
224
|
+
gps_fix_type=data.gps_fix_type,
|
|
225
|
+
horizontal_accuracy=data.horizontal_accuracy,
|
|
226
|
+
vertical_accuracy=data.vertical_accuracy,
|
|
227
|
+
velocity_east=data.velocity_east,
|
|
228
|
+
velocity_north=data.velocity_north,
|
|
229
|
+
velocity_up=data.velocity_up,
|
|
230
|
+
speed_accuracy=data.speed_accuracy,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def serialize(cls, data: telemetry.CAMMGPSPoint) -> bytes:
|
|
235
|
+
cls.serializable(data, throw=True)
|
|
236
|
+
|
|
237
|
+
return CAMMSampleData.build(
|
|
238
|
+
{
|
|
239
|
+
"type": cls.serialized_camm_type.value,
|
|
240
|
+
"data": {
|
|
241
|
+
"time_gps_epoch": data.time_gps_epoch,
|
|
242
|
+
"gps_fix_type": data.gps_fix_type,
|
|
243
|
+
"latitude": data.lat,
|
|
244
|
+
"longitude": data.lon,
|
|
245
|
+
"altitude": -1.0 if data.alt is None else data.alt,
|
|
246
|
+
"horizontal_accuracy": data.horizontal_accuracy,
|
|
247
|
+
"vertical_accuracy": data.vertical_accuracy,
|
|
248
|
+
"velocity_east": data.velocity_east,
|
|
249
|
+
"velocity_north": data.velocity_north,
|
|
250
|
+
"velocity_up": data.velocity_up,
|
|
251
|
+
"speed_accuracy": data.speed_accuracy,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
110
254
|
)
|
|
111
|
-
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class GoProGPSSampleEntry(CAMMSampleEntry):
|
|
258
|
+
serialized_camm_type: CAMMType = CAMMType.MIN_GPS
|
|
259
|
+
|
|
260
|
+
telemetry_cls_type = telemetry.GPSPoint
|
|
261
|
+
|
|
262
|
+
construct = _Double[3] # type: ignore
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GPSPoint:
|
|
266
|
+
raise NotImplementedError("Deserializing GoPro GPS Point is not supported")
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def serialize(cls, data: telemetry.GPSPoint) -> bytes:
|
|
270
|
+
cls.serializable(data, throw=True)
|
|
271
|
+
|
|
272
|
+
return CAMMSampleData.build(
|
|
273
|
+
{
|
|
274
|
+
"type": cls.serialized_camm_type.value,
|
|
275
|
+
"data": [
|
|
276
|
+
data.lat,
|
|
277
|
+
data.lon,
|
|
278
|
+
-1.0 if data.alt is None else data.alt,
|
|
279
|
+
],
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class AccelerationSampleEntry(CAMMSampleEntry):
|
|
285
|
+
serialized_camm_type: CAMMType = CAMMType.ACCELERATION
|
|
286
|
+
|
|
287
|
+
telemetry_cls_type = telemetry.AccelerationData
|
|
288
|
+
|
|
289
|
+
construct: C.Struct = _Float[3] # type: ignore
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.AccelerationData:
|
|
112
293
|
return telemetry.AccelerationData(
|
|
113
294
|
time=sample.exact_time,
|
|
114
|
-
x=
|
|
115
|
-
y=
|
|
116
|
-
z=
|
|
295
|
+
x=data[0],
|
|
296
|
+
y=data[1],
|
|
297
|
+
z=data[2],
|
|
117
298
|
)
|
|
118
|
-
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def serialize(cls, data: telemetry.AccelerationData) -> bytes:
|
|
302
|
+
cls.serializable(data, throw=True)
|
|
303
|
+
|
|
304
|
+
return CAMMSampleData.build(
|
|
305
|
+
{
|
|
306
|
+
"type": cls.serialized_camm_type.value,
|
|
307
|
+
"data": [data.x, data.y, data.z],
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class GyroscopeSampleEntry(CAMMSampleEntry):
|
|
313
|
+
serialized_camm_type: CAMMType = CAMMType.GYRO
|
|
314
|
+
|
|
315
|
+
telemetry_cls_type = telemetry.GyroscopeData
|
|
316
|
+
|
|
317
|
+
construct: C.Struct = _Float[3] # type: ignore
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GyroscopeData:
|
|
119
321
|
return telemetry.GyroscopeData(
|
|
120
322
|
time=sample.exact_time,
|
|
121
|
-
x=
|
|
122
|
-
y=
|
|
123
|
-
z=
|
|
323
|
+
x=data[0],
|
|
324
|
+
y=data[1],
|
|
325
|
+
z=data[2],
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def serialize(cls, data: telemetry.GyroscopeData) -> bytes:
|
|
330
|
+
cls.serializable(data)
|
|
331
|
+
|
|
332
|
+
return CAMMSampleData.build(
|
|
333
|
+
{
|
|
334
|
+
"type": cls.serialized_camm_type.value,
|
|
335
|
+
"data": [data.x, data.y, data.z],
|
|
336
|
+
}
|
|
124
337
|
)
|
|
125
|
-
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class MagnetometerSampleEntry(CAMMSampleEntry):
|
|
341
|
+
serialized_camm_type: CAMMType = CAMMType.MAGNETIC_FIELD
|
|
342
|
+
|
|
343
|
+
telemetry_cls_type = telemetry.MagnetometerData
|
|
344
|
+
|
|
345
|
+
construct: C.Struct = _Float[3] # type: ignore
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.MagnetometerData:
|
|
126
349
|
return telemetry.MagnetometerData(
|
|
127
350
|
time=sample.exact_time,
|
|
128
|
-
x=
|
|
129
|
-
y=
|
|
130
|
-
z=
|
|
351
|
+
x=data[0],
|
|
352
|
+
y=data[1],
|
|
353
|
+
z=data[2],
|
|
131
354
|
)
|
|
132
|
-
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def serialize(cls, data: telemetry.MagnetometerData) -> bytes:
|
|
358
|
+
cls.serializable(data)
|
|
359
|
+
|
|
360
|
+
return CAMMSampleData.build(
|
|
361
|
+
{
|
|
362
|
+
"type": cls.serialized_camm_type.value,
|
|
363
|
+
"data": [data.x, data.y, data.z],
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
SAMPLE_ENTRY_CLS_BY_CAMM_TYPE = {
|
|
369
|
+
sample_entry_cls.serialized_camm_type: sample_entry_cls
|
|
370
|
+
for sample_entry_cls in CAMMSampleEntry.__subclasses__()
|
|
371
|
+
if sample_entry_cls not in [GoProGPSSampleEntry]
|
|
372
|
+
}
|
|
373
|
+
assert len(SAMPLE_ENTRY_CLS_BY_CAMM_TYPE) == 5, SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.keys()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
_SWITCH: T.Dict[int, C.Struct] = {
|
|
377
|
+
# Angle_axis
|
|
378
|
+
CAMMType.ANGLE_AXIS.value: _Float[3], # type: ignore
|
|
379
|
+
# Exposure time
|
|
380
|
+
CAMMType.EXPOSURE_TIME.value: C.Struct(
|
|
381
|
+
"pixel_exposure_time" / C.Int32sl, # type: ignore
|
|
382
|
+
"rolling_shutter_skew_time" / C.Int32sl, # type: ignore
|
|
383
|
+
),
|
|
384
|
+
# Position
|
|
385
|
+
CAMMType.POSITION.value: _Float[3], # type: ignore
|
|
386
|
+
# Serializable types
|
|
387
|
+
**{t.value: cls.construct for t, cls in SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.items()},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _construct_with_selected_camm_types(
|
|
392
|
+
selected_camm_types: T.Container[CAMMType] | None = None,
|
|
393
|
+
) -> C.Struct:
|
|
394
|
+
if selected_camm_types is None:
|
|
395
|
+
switch = _SWITCH
|
|
396
|
+
else:
|
|
397
|
+
switch = {
|
|
398
|
+
k: v for k, v in _SWITCH.items() if CAMMType(k) in selected_camm_types
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return C.Struct(
|
|
402
|
+
C.Padding(2),
|
|
403
|
+
"type" / C.Int16ul,
|
|
404
|
+
"data" / C.Switch(C.this.type, switch),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
CAMMSampleData = _construct_with_selected_camm_types()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _parse_telemetry_from_sample(
|
|
412
|
+
fp: T.BinaryIO,
|
|
413
|
+
sample: Sample,
|
|
414
|
+
construct: C.Struct | None = None,
|
|
415
|
+
) -> TelemetryMeasurement | None:
|
|
416
|
+
if construct is None:
|
|
417
|
+
construct = CAMMSampleData
|
|
418
|
+
|
|
419
|
+
fp.seek(sample.raw_sample.offset, io.SEEK_SET)
|
|
420
|
+
data = fp.read(sample.raw_sample.size)
|
|
421
|
+
|
|
422
|
+
box = construct.parse(data)
|
|
423
|
+
|
|
424
|
+
# boxdata=None when the construct is unable to parse the data
|
|
425
|
+
# (CAMM type not in the switch)
|
|
426
|
+
if box.data is None:
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
camm_type = CAMMType(box.type) # type: ignore
|
|
430
|
+
SampleKlass = SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.get(camm_type)
|
|
431
|
+
if SampleKlass is None:
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
return SampleKlass.deserialize(sample, box.data)
|
|
133
435
|
|
|
134
436
|
|
|
135
437
|
def _filter_telemetry_by_elst_segments(
|
|
@@ -188,7 +490,7 @@ def _filter_telemetry_by_track_elst(
|
|
|
188
490
|
moov: MovieBoxParser,
|
|
189
491
|
track: TrackBoxParser,
|
|
190
492
|
measurements: T.Iterable[TelemetryMeasurement],
|
|
191
|
-
) ->
|
|
493
|
+
) -> list[TelemetryMeasurement]:
|
|
192
494
|
elst_boxdata = track.extract_elst_boxdata()
|
|
193
495
|
|
|
194
496
|
if elst_boxdata is not None:
|
|
@@ -216,127 +518,73 @@ def _filter_telemetry_by_track_elst(
|
|
|
216
518
|
return list(measurements)
|
|
217
519
|
|
|
218
520
|
|
|
219
|
-
|
|
220
|
-
"""
|
|
221
|
-
Return a list of points (could be empty) if it is a valid CAMM video,
|
|
222
|
-
otherwise None
|
|
223
|
-
"""
|
|
224
|
-
|
|
225
|
-
moov = MovieBoxParser.parse_stream(fp)
|
|
226
|
-
|
|
227
|
-
for track in moov.extract_tracks():
|
|
228
|
-
if _contains_camm_description(track):
|
|
229
|
-
maybe_measurements = (
|
|
230
|
-
_parse_telemetry_from_sample(fp, sample)
|
|
231
|
-
for sample in track.extract_samples()
|
|
232
|
-
if _is_camm_description(sample.description)
|
|
233
|
-
)
|
|
234
|
-
points = [m for m in maybe_measurements if isinstance(m, geo.Point)]
|
|
235
|
-
|
|
236
|
-
return T.cast(
|
|
237
|
-
T.List[geo.Point], _filter_telemetry_by_track_elst(moov, track, points)
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
return None
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def extract_telemetry_data(fp: T.BinaryIO) -> T.Optional[T.List[TelemetryMeasurement]]:
|
|
244
|
-
moov = MovieBoxParser.parse_stream(fp)
|
|
245
|
-
|
|
246
|
-
for track in moov.extract_tracks():
|
|
247
|
-
if _contains_camm_description(track):
|
|
248
|
-
maybe_measurements = (
|
|
249
|
-
_parse_telemetry_from_sample(fp, sample)
|
|
250
|
-
for sample in track.extract_samples()
|
|
251
|
-
if _is_camm_description(sample.description)
|
|
252
|
-
)
|
|
253
|
-
measurements = [m for m in maybe_measurements if m is not None]
|
|
254
|
-
|
|
255
|
-
measurements = _filter_telemetry_by_track_elst(moov, track, measurements)
|
|
256
|
-
|
|
257
|
-
return measurements
|
|
258
|
-
|
|
259
|
-
return None
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def parse_gpx(path: pathlib.Path) -> T.List[geo.Point]:
|
|
263
|
-
with path.open("rb") as fp:
|
|
264
|
-
points = extract_points(fp)
|
|
265
|
-
if points is None:
|
|
266
|
-
return []
|
|
267
|
-
return points
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
MakeOrModel = C.Struct(
|
|
521
|
+
_MakeOrModel = C.Struct(
|
|
271
522
|
"size" / C.Int16ub,
|
|
272
523
|
C.Padding(2),
|
|
273
524
|
"data" / C.FixedSized(C.this.size, C.GreedyBytes),
|
|
274
525
|
)
|
|
275
526
|
|
|
276
527
|
|
|
277
|
-
def _decode_quietly(data: bytes,
|
|
528
|
+
def _decode_quietly(data: bytes, type: bytes) -> str:
|
|
278
529
|
try:
|
|
279
530
|
return data.decode("utf-8")
|
|
280
531
|
except UnicodeDecodeError:
|
|
281
|
-
LOG.warning("Failed to decode %s: %s",
|
|
532
|
+
LOG.warning("Failed to decode %s: %s", type, data[:512])
|
|
282
533
|
return ""
|
|
283
534
|
|
|
284
535
|
|
|
285
|
-
def _parse_quietly(data: bytes,
|
|
536
|
+
def _parse_quietly(data: bytes, type: bytes) -> bytes:
|
|
286
537
|
try:
|
|
287
|
-
parsed =
|
|
538
|
+
parsed = _MakeOrModel.parse(data)
|
|
288
539
|
except C.ConstructError:
|
|
289
|
-
LOG.warning("Failed to parse %s: %s",
|
|
540
|
+
LOG.warning("Failed to parse %s: %s", type, data[:512])
|
|
290
541
|
return b""
|
|
291
|
-
return parsed["data"]
|
|
292
542
|
|
|
543
|
+
if parsed is None:
|
|
544
|
+
return b""
|
|
293
545
|
|
|
294
|
-
|
|
295
|
-
header_and_stream = sparser.parse_path(
|
|
296
|
-
fp,
|
|
297
|
-
[
|
|
298
|
-
b"moov",
|
|
299
|
-
b"udta",
|
|
300
|
-
[
|
|
301
|
-
# Insta360 Titan
|
|
302
|
-
b"\xa9mak",
|
|
303
|
-
b"\xa9mod",
|
|
304
|
-
# RICHO THETA V
|
|
305
|
-
b"@mod",
|
|
306
|
-
b"@mak",
|
|
307
|
-
# RICHO THETA V
|
|
308
|
-
b"manu",
|
|
309
|
-
b"modl",
|
|
310
|
-
],
|
|
311
|
-
],
|
|
312
|
-
)
|
|
546
|
+
return parsed["data"]
|
|
313
547
|
|
|
314
|
-
make: T.Optional[str] = None
|
|
315
|
-
model: T.Optional[str] = None
|
|
316
548
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
549
|
+
def _extract_camera_make_and_model_from_utda_boxdata(
|
|
550
|
+
utda_boxdata: dict,
|
|
551
|
+
) -> tuple[str, str]:
|
|
552
|
+
make: str = ""
|
|
553
|
+
model: str = ""
|
|
554
|
+
|
|
555
|
+
for box in utda_boxdata:
|
|
556
|
+
# Insta360 Titan
|
|
557
|
+
if box.type == b"\xa9mak":
|
|
558
|
+
if not make:
|
|
559
|
+
make_data = _parse_quietly(box.data, box.type)
|
|
322
560
|
make_data = make_data.rstrip(b"\x00")
|
|
323
|
-
make = _decode_quietly(make_data,
|
|
324
|
-
|
|
325
|
-
|
|
561
|
+
make = _decode_quietly(make_data, box.type)
|
|
562
|
+
|
|
563
|
+
# Insta360 Titan
|
|
564
|
+
elif box.type == b"\xa9mod":
|
|
565
|
+
if not model:
|
|
566
|
+
model_data = _parse_quietly(box.data, box.type)
|
|
326
567
|
model_data = model_data.rstrip(b"\x00")
|
|
327
|
-
model = _decode_quietly(model_data,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
568
|
+
model = _decode_quietly(model_data, box.type)
|
|
569
|
+
|
|
570
|
+
# RICHO THETA V
|
|
571
|
+
elif box.type in [b"@mak", b"manu"]:
|
|
572
|
+
if not make:
|
|
573
|
+
make = _decode_quietly(box.data, box.type)
|
|
574
|
+
|
|
575
|
+
# RICHO THETA V
|
|
576
|
+
elif box.type in [b"@mod", b"modl"]:
|
|
577
|
+
if not model:
|
|
578
|
+
model = _decode_quietly(box.data, box.type)
|
|
579
|
+
|
|
580
|
+
# quit when both found
|
|
581
|
+
if make and model:
|
|
582
|
+
break
|
|
337
583
|
|
|
338
584
|
if make:
|
|
339
585
|
make = make.strip()
|
|
586
|
+
|
|
340
587
|
if model:
|
|
341
588
|
model = model.strip()
|
|
342
|
-
|
|
589
|
+
|
|
590
|
+
return make, model
|