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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +94 -4
- mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
- mapillary_tools/camm/camm_parser.py +561 -0
- mapillary_tools/commands/__init__.py +0 -1
- mapillary_tools/commands/__main__.py +0 -6
- mapillary_tools/commands/process.py +0 -50
- mapillary_tools/commands/upload.py +1 -26
- mapillary_tools/constants.py +2 -2
- mapillary_tools/exiftool_read_video.py +13 -11
- mapillary_tools/ffmpeg.py +2 -2
- mapillary_tools/geo.py +0 -54
- mapillary_tools/geotag/blackvue_parser.py +4 -4
- mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
- mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
- mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
- mapillary_tools/geotag/gpmf_parser.py +346 -83
- mapillary_tools/mp4/__init__.py +0 -0
- mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
- mapillary_tools/mp4/mp4_sample_parser.py +322 -0
- mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
- mapillary_tools/process_geotag_properties.py +25 -19
- mapillary_tools/process_sequence_properties.py +6 -6
- mapillary_tools/sample_video.py +17 -16
- mapillary_tools/telemetry.py +71 -0
- mapillary_tools/types.py +18 -0
- mapillary_tools/upload.py +74 -233
- mapillary_tools/upload_api_v4.py +8 -9
- mapillary_tools/utils.py +9 -16
- mapillary_tools/video_data_extraction/cli_options.py +0 -1
- mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
- mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
- mapillary_tools-0.13.1.dist-info/RECORD +75 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
- mapillary_tools/commands/upload_blackvue.py +0 -33
- mapillary_tools/commands/upload_camm.py +0 -33
- mapillary_tools/commands/upload_zip.py +0 -33
- mapillary_tools/geotag/camm_parser.py +0 -306
- mapillary_tools/geotag/mp4_sample_parser.py +0 -426
- mapillary_tools/process_import_meta_properties.py +0 -76
- mapillary_tools-0.12.1.dist-info/RECORD +0 -77
- /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
- /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/top_level.txt +0 -0
|
@@ -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 ""
|
|
@@ -12,9 +12,6 @@ from . import (
|
|
|
12
12
|
process_and_upload,
|
|
13
13
|
sample_video,
|
|
14
14
|
upload,
|
|
15
|
-
upload_blackvue,
|
|
16
|
-
upload_camm,
|
|
17
|
-
upload_zip,
|
|
18
15
|
video_process,
|
|
19
16
|
video_process_and_upload,
|
|
20
17
|
zip,
|
|
@@ -23,9 +20,6 @@ from . import (
|
|
|
23
20
|
mapillary_tools_commands = [
|
|
24
21
|
process,
|
|
25
22
|
upload,
|
|
26
|
-
upload_camm,
|
|
27
|
-
upload_blackvue,
|
|
28
|
-
upload_zip,
|
|
29
23
|
sample_video,
|
|
30
24
|
video_process,
|
|
31
25
|
authenticate,
|
|
@@ -10,7 +10,6 @@ from ..process_geotag_properties import (
|
|
|
10
10
|
process_finalize,
|
|
11
11
|
process_geotag_properties,
|
|
12
12
|
)
|
|
13
|
-
from ..process_import_meta_properties import process_import_meta_properties
|
|
14
13
|
from ..process_sequence_properties import process_sequence_properties
|
|
15
14
|
|
|
16
15
|
|
|
@@ -107,44 +106,6 @@ class Command:
|
|
|
107
106
|
default=None,
|
|
108
107
|
required=False,
|
|
109
108
|
)
|
|
110
|
-
group_metadata.add_argument(
|
|
111
|
-
"--add_file_name",
|
|
112
|
-
help="[DEPRECATED since v0.9.4] Add original file name to EXIF.",
|
|
113
|
-
action="store_true",
|
|
114
|
-
required=False,
|
|
115
|
-
)
|
|
116
|
-
group_metadata.add_argument(
|
|
117
|
-
"--add_import_date",
|
|
118
|
-
help="[DEPRECATED since v0.10.0] Add import date.",
|
|
119
|
-
action="store_true",
|
|
120
|
-
required=False,
|
|
121
|
-
)
|
|
122
|
-
group_metadata.add_argument(
|
|
123
|
-
"--orientation",
|
|
124
|
-
help="Specify the image orientation in degrees. Note this might result in image rotation. Note this input has precedence over the input read from the import source file.",
|
|
125
|
-
choices=[0, 90, 180, 270],
|
|
126
|
-
type=int,
|
|
127
|
-
default=None,
|
|
128
|
-
required=False,
|
|
129
|
-
)
|
|
130
|
-
group_metadata.add_argument(
|
|
131
|
-
"--GPS_accuracy",
|
|
132
|
-
help="GPS accuracy in meters. Note this input has precedence over the input read from the import source file.",
|
|
133
|
-
default=None,
|
|
134
|
-
required=False,
|
|
135
|
-
)
|
|
136
|
-
group_metadata.add_argument(
|
|
137
|
-
"--camera_uuid",
|
|
138
|
-
help="Custom string used to differentiate different captures taken with the same camera make and model.",
|
|
139
|
-
default=None,
|
|
140
|
-
required=False,
|
|
141
|
-
)
|
|
142
|
-
group_metadata.add_argument(
|
|
143
|
-
"--custom_meta_data",
|
|
144
|
-
help='[DEPRECATED since v0.10.0] Add custom meta data to all images. Required format of input is a string, consisting of the meta data name, type and value, separated by a comma for each entry, where entries are separated by semicolon. Supported types are long, double, string, boolean, date. Example for two meta data entries "random_name1,double,12.34;random_name2,long,1234".',
|
|
145
|
-
default=None,
|
|
146
|
-
required=False,
|
|
147
|
-
)
|
|
148
109
|
|
|
149
110
|
group_geotagging = parser.add_argument_group(
|
|
150
111
|
f"{constants.ANSI_BOLD}PROCESS GEOTAGGING OPTIONS{constants.ANSI_RESET_ALL}"
|
|
@@ -278,17 +239,6 @@ class Command:
|
|
|
278
239
|
),
|
|
279
240
|
)
|
|
280
241
|
|
|
281
|
-
metadatas = process_import_meta_properties(
|
|
282
|
-
metadatas=metadatas,
|
|
283
|
-
**(
|
|
284
|
-
{
|
|
285
|
-
k: v
|
|
286
|
-
for k, v in vars_args.items()
|
|
287
|
-
if k in inspect.getfullargspec(process_import_meta_properties).args
|
|
288
|
-
}
|
|
289
|
-
),
|
|
290
|
-
)
|
|
291
|
-
|
|
292
242
|
metadatas = process_sequence_properties(
|
|
293
243
|
metadatas=metadatas,
|
|
294
244
|
**(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
import typing as T
|
|
3
2
|
|
|
4
3
|
from .. import constants
|
|
5
|
-
from ..upload import
|
|
4
|
+
from ..upload import upload
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class Command:
|
|
@@ -34,30 +33,6 @@ class Command:
|
|
|
34
33
|
group = parser.add_argument_group(
|
|
35
34
|
f"{constants.ANSI_BOLD}UPLOAD OPTIONS{constants.ANSI_RESET_ALL}"
|
|
36
35
|
)
|
|
37
|
-
default_filetypes = ",".join(sorted(t.value for t in FileType))
|
|
38
|
-
supported_filetypes = ",".join(
|
|
39
|
-
sorted(
|
|
40
|
-
[t.value for t in DirectUploadFileType] + [t.value for t in FileType]
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
def _type(option: str) -> T.List[T.Union[FileType, DirectUploadFileType]]:
|
|
45
|
-
r: T.List[T.Union[FileType, DirectUploadFileType]] = []
|
|
46
|
-
for t in option.split(","):
|
|
47
|
-
if t in [x.value for x in FileType]:
|
|
48
|
-
r.append(FileType(t))
|
|
49
|
-
else:
|
|
50
|
-
r.append(DirectUploadFileType(t))
|
|
51
|
-
return r
|
|
52
|
-
|
|
53
|
-
group.add_argument(
|
|
54
|
-
"--filetypes",
|
|
55
|
-
"--file_types",
|
|
56
|
-
help=f"Upload files of the specified types only. Supported file types: {supported_filetypes} [default: %(default)s]",
|
|
57
|
-
type=_type,
|
|
58
|
-
default=default_filetypes,
|
|
59
|
-
required=False,
|
|
60
|
-
)
|
|
61
36
|
group.add_argument(
|
|
62
37
|
"--desc_path",
|
|
63
38
|
help=f'Path to the description file generated by the process command. The hyphen "-" indicates STDIN. [default: {{IMPORT_PATH}}/{constants.IMAGE_DESCRIPTION_FILENAME}]',
|
mapillary_tools/constants.py
CHANGED
|
@@ -43,8 +43,8 @@ GOPRO_GPS_PRECISION = float(os.getenv(_ENV_PREFIX + "GOPRO_GPS_PRECISION", 15))
|
|
|
43
43
|
|
|
44
44
|
# WARNING: Changing the following envvars might result in failed uploads
|
|
45
45
|
# Max number of images per sequence
|
|
46
|
-
MAX_SEQUENCE_LENGTH = int(os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH",
|
|
46
|
+
MAX_SEQUENCE_LENGTH = int(os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH", 1000))
|
|
47
47
|
# Max file size per sequence (sum of image filesizes in the sequence)
|
|
48
|
-
MAX_SEQUENCE_FILESIZE: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "
|
|
48
|
+
MAX_SEQUENCE_FILESIZE: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "10G")
|
|
49
49
|
# Max number of pixels per sequence (sum of image pixels in the sequence)
|
|
50
50
|
MAX_SEQUENCE_PIXELS: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_PIXELS", "6G")
|